From 43fc95c3e35261ec0a8e1d0c38b7b70adf6e0dda Mon Sep 17 00:00:00 2001 From: Dregu Date: Mon, 9 Oct 2023 01:50:14 +0300 Subject: [PATCH 01/29] add levelgen flags --- src/game_api/flags.hpp | 33 +++++++++++++++++++++++++++++++++ src/injected/ui.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/game_api/flags.hpp b/src/game_api/flags.hpp index 79aaa2c53..7d01b0f30 100644 --- a/src/game_api/flags.hpp +++ b/src/game_api/flags.hpp @@ -756,3 +756,36 @@ const char* pause_types[]{ "32: Ankh (smooth camera, janky audio)", "Freeze on PRE_UPDATE", // this is not a real state.pause flag, it's only used by ui.cpp for magic }; + +const char* levelgen_flags[]{ + "1: Should generate path", + "2: Can spawn vault", + "3: Can spawn shops", + "4: Can have outpost?", + "5: Should spawn hard floor decorations", + "6: Apply ambient occlusion", + "7: Should spawn behind-floor and below-floorstyled decorations", + "8: unknown", +}; + +const char* levelgen_flags2[]{ + "1: Spawns background decorations on ground (ceiling if false)", + "2: Spawns fake ladder/chain midbg?", + "3: Spawn entrance door background (Ignored in 7-1 to 7-2 transition)", + "4: Procedural backlayer door midbg indicator related", + "5: Spawn backlayer border/background", + "6: Should spawn procedural backlayers", + "7: Should spawn backlayer torches", + "8: Has ghost", +}; + +const char* levelgen_flags3[]{ + "1: Can spawn angry NPCs", + "2: Can echo", + "3: Can spawn Dead are Restless", + "4: Can spawn procedural skeletons", + "5: Can have quests?", + "6: Can spawn player coffins", + "7: unknown", + "8: unknown", +}; diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index b5735dc31..f311144d6 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -8011,6 +8011,35 @@ void render_game_props() } endmenu(); } + if (submenu("Level generation flags")) + { + ImGui::Text("Flags 1:"); + auto flags = (int)g_state->level_gen->flags; + auto flags2 = (int)g_state->level_gen->flags2; + auto flags3 = (int)g_state->level_gen->flags3; + for (int i = 0; i < 8; i++) + { + ImGui::CheckboxFlags(levelgen_flags[i], &flags, (int)std::pow(2, i)); + } + ImGui::Text("Flags 2:"); + for (int i = 0; i < 8; i++) + { + ImGui::CheckboxFlags(levelgen_flags2[i], &flags2, (int)std::pow(2, i)); + } + ImGui::Text("Flags 3:"); + for (int i = 0; i < 8; i++) + { + ImGui::CheckboxFlags(levelgen_flags3[i], &flags3, (int)std::pow(2, i)); + } + g_state->level_gen->flags = (uint8_t)flags; + g_state->level_gen->flags2 = (uint8_t)flags2; + g_state->level_gen->flags3 = (uint8_t)flags3; + ImGui::Text("Theme flags:"); + ImGui::Checkbox("Allow beehives##ThemeBeeHive", &g_state->current_theme->allow_beehive); + ImGui::Checkbox("Allow leprechauns##ThemeLeprechaun", &g_state->current_theme->allow_leprechaun); + + endmenu(); + } if (submenu("AI targets")) { for (size_t x = 0; x < 8; ++x) From b166e9f82a4389e10871d867a7682526c7b62f1c Mon Sep 17 00:00:00 2001 From: Dregu Date: Wed, 11 Oct 2023 09:47:23 +0300 Subject: [PATCH 02/29] Use destroy_grid in ui delete --- src/injected/ui.cpp | 10 +++------- src/injected/ui_util.cpp | 7 ++++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index f311144d6..d8a3e2f28 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -1234,6 +1234,7 @@ 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"); + UI::safe_destroy(ent, unsafe); if ((ent->type->id >= first_door && ent->type->id <= first_door + 15) || ent->type->id == logical_door) { auto pos = ent->position(); @@ -1245,14 +1246,9 @@ void smart_delete(Entity* ent, bool unsafe = false) auto pos = ent->position(); auto layer = (LAYER)ent->layer; ENT_TYPE type = ent->type->id; - Callback cb = {g_state->time_total + 1, [pos, layer, type] - { - fix_decorations_at(std::round(pos.first), std::round(pos.second), layer); - UI::cleanup_at(std::round(pos.first), std::round(pos.second), layer, type); - }}; - callbacks.push_back(cb); + fix_decorations_at(std::round(pos.first), std::round(pos.second), layer); + UI::cleanup_at(std::round(pos.first), std::round(pos.second), layer, type); } - UI::safe_destroy(ent, unsafe); } void reset_windows() diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index d74586c2f..3da8c84ef 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -699,10 +699,11 @@ void UI::safe_destroy(Entity* ent, bool unsafe, bool recurse) const auto [x, y] = UI::get_position(ent); const auto sf = ent->type->search_flags; destroy_entity_items(ent); - if (sf & 0x100 && test_flag(ent->flags, 3)) // solid floor + if (sf & 0x100) { - ent->destroy(); - update_liquid_collision_at(x, y, false); + if (test_flag(ent->flags, 3)) // solid floor + update_liquid_collision_at(x, y, false); + destroy_grid(ent->uid); } else if (ent->is_liquid()) { From ff63713feca0fdcee9957bcb7b777022c12f83a7 Mon Sep 17 00:00:00 2001 From: Dregu Date: Wed, 11 Oct 2023 10:04:33 +0300 Subject: [PATCH 03/29] hitbox color fixes --- src/injected/ui.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index d8a3e2f28..27c450eb4 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -1330,7 +1330,7 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true) if (to_spawn.name.find("ENT_TYPE_CHAR") != std::string::npos) { int spawned = UI::spawn_companion(to_spawn.id, cpos.first, cpos.second, LAYER::PLAYER, g_vx, g_vy); - if (!lock_entity && set_last) + if (!lock_entity && set_last && options["draw_hitboxes"]) g_last_id = spawned; return spawned; } @@ -1344,7 +1344,7 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true) static const auto ana_spelunky = to_id("ENT_TYPE_CHAR_ANA_SPELUNKY"); auto spawned = UI::spawn_playerghost(ana_spelunky + (rand() % 19), cpos.first, cpos.second, LAYER::PLAYER, g_vx, g_vy); - if (!lock_entity && set_last) + if (!lock_entity && set_last && options["draw_hitboxes"]) g_last_id = spawned; return spawned; } @@ -1418,7 +1418,7 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true) ent->door_type = to_id("ENT_TYPE_FLOOR_DOOR_LAYER"); ent->platform_type = to_id("ENT_TYPE_FLOOR_DOOR_PLATFORM"); } - if (!lock_entity && set_last) + if (!lock_entity && set_last && options["draw_hitboxes"]) g_last_id = spawned; return spawned; } @@ -4727,21 +4727,26 @@ void render_clickhandler() } } + static auto front_col = ImColor(0, 255, 51, 200); + static auto back_col = ImColor(255, 160, 31, 200); + static auto front_fill = ImColor(front_col); + front_fill.Value.w = 0.25f; + static auto back_fill = ImColor(back_col); + back_fill.Value.w = 0.25f; if (update_entity()) { - render_hitbox(g_entity, true, ImColor(0, 255, 0, 200)); + auto this_layer = (peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer) == g_entity->layer; + render_hitbox(g_entity, true, this_layer ? front_col : back_col); } - static auto front_col = ImColor(0, 255, 51, 100); - static auto back_col = ImColor(255, 160, 31, 100); for (auto entity : g_selected_ids) { auto ent = get_entity_ptr(entity); if (ent) { if (ent->layer == (peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer)) - render_hitbox(ent, false, front_col, true); + render_hitbox(ent, false, front_fill, true); else - render_hitbox(ent, false, back_col, true); + render_hitbox(ent, false, back_fill, true); } } From 2dd67d9b47eabf41a12873ca3487ed52607decbb Mon Sep 17 00:00:00 2001 From: Dregu Date: Wed, 11 Oct 2023 13:59:05 +0300 Subject: [PATCH 04/29] add ui warp buttons to special game screens --- src/injected/ui.cpp | 62 ++++++++++++++++++++++++++++++++++++++-- src/injected/ui_util.cpp | 5 ++++ src/injected/ui_util.hpp | 1 + 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 27c450eb4..318d8ebc9 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -1886,8 +1886,6 @@ void quick_start(uint8_t screen, uint8_t world, uint8_t level, uint8_t theme) g_state->level_next = level; g_state->theme_next = theme; g_state->quest_flags = g_state->quest_flags | 1; - g_state->fadein = 1; - g_state->fadeout = 1; g_state->loading = 1; if (g_game_manager->main_menu_music) @@ -3515,6 +3513,66 @@ const char* theme_name(int theme) void render_narnia() { + if (submenu("Other game screens")) + { + int screen = -1; + ImGui::PushID("WarpSpecial"); + for (unsigned int i = 0; i < 21; ++i) + { + if ((i >= 5 && i <= 10)) + continue; + if (options["menu_ui"]) + { + if (ImGui::MenuItem(screen_names[i])) + screen = i; + } + else + { + if (i % 2) + ImGui::SameLine(ImGui::GetContentRegionAvail().x * 0.5f); + if (ImGui::Button(screen_names[i], ImVec2(ImGui::GetContentRegionMax().x * 0.5f, 0))) + screen = i; + } + } + endmenu(); + if (screen != -1) + { + if (screen == 14) + { + if (g_state->screen == 11 or g_state->screen == 12) + UI::load_death_screen(); + } + else if (g_state->screen != 12 && screen >= 11) + { + quick_start((uint8_t)screen, 1, 1, 1); + } + else + { + g_state->screen_next = screen; + g_state->loading = 1; + } + if (screen >= 16 && screen <= 18) + { + g_state->win_state = 1; + if (!g_state->end_spaceship_character) + g_state->end_spaceship_character = to_id("ENT_TYPE_CHAR_EGGPLANT_CHILD"); + } + if (screen == 19) + { + g_state->world_next = 8; + g_state->level_next = 99; + g_state->theme_next = 10; + if (!g_state->level_gen->theme_cosmicocean->sub_theme) + g_state->level_gen->theme_cosmicocean->sub_theme = g_state->level_gen->theme_dwelling; + g_state->current_theme = g_state->level_gen->theme_cosmicocean; + g_state->win_state = 3; + if (g_state->level_count < 1) + g_state->level_count = 1; + } + } + ImGui::PopID(); + } + ImGui::Text("Next level"); ImGui::SameLine(100.0f); diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index 3da8c84ef..95ae45c26 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -759,3 +759,8 @@ std::pair UI::spawn_position() { return {State::get().ptr()->level_gen->spawn_x, State::get().ptr()->level_gen->spawn_y}; } + +void UI::load_death_screen() +{ + call_death_screen(); +} diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp index d5960d5d1..4fd364e2a 100644 --- a/src/injected/ui_util.hpp +++ b/src/injected/ui_util.hpp @@ -86,4 +86,5 @@ class UI static int32_t spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer, float vx, float vy); static void spawn_player(uint8_t player_slot, float x, float y); static std::pair spawn_position(); + static void load_death_screen(); }; From 26e3165773e021d0b4fd2c67eb6fca879c60d4f3 Mon Sep 17 00:00:00 2001 From: Dregu Date: Wed, 11 Oct 2023 15:17:41 +0300 Subject: [PATCH 05/29] Add unload level callbacks --- src/game_api/level_api.cpp | 16 +++++ src/game_api/rpc.cpp | 22 +++++++ src/game_api/rpc.hpp | 2 + src/game_api/script/events.cpp | 41 +++++++++++++ src/game_api/script/events.hpp | 6 ++ src/game_api/script/lua_backend.cpp | 95 +++++++++++++++++++++++++++++ src/game_api/script/lua_backend.hpp | 12 +++- src/game_api/script/lua_vm.cpp | 26 +++++++- src/game_api/search.cpp | 9 +++ 9 files changed, 227 insertions(+), 2 deletions(-) diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index 226e3e854..dd3cf3e6f 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -856,6 +856,20 @@ void load_screen(StateMemory* state, size_t param_2, size_t param_3) post_load_screen(); } +using UnloadLayerFun = void(Layer*); +UnloadLayerFun* g_unload_layer_trampoline{nullptr}; +void unload_layer(Layer* layer) +{ + if (!layer->is_back_layer && pre_unload_level()) + return; + if (pre_unload_layer((LAYER)layer->is_back_layer)) + return; + g_unload_layer_trampoline(layer); + post_unload_layer((LAYER)layer->is_back_layer); + if (layer->is_back_layer) + post_unload_level(); +} + using HandleTileCodeFun = void(LevelGenSystem*, std::uint32_t, std::uint64_t, float, float, std::uint8_t); HandleTileCodeFun* g_handle_tile_code_trampoline{nullptr}; void handle_tile_code(LevelGenSystem* self, std::uint32_t tile_code, std::uint16_t room_template, float x, float y, std::uint8_t layer) @@ -1470,6 +1484,7 @@ void LevelGenData::init() g_spawn_room_from_tile_codes_trampoline = (SpawnRoomFromTileCodes*)get_address("level_gen_spawn_room_from_tile_codes"sv); g_load_screen_trampoline = (LoadScreenFun*)get_address("load_screen_func"sv); + g_unload_layer_trampoline = (UnloadLayerFun*)get_address("unload_layer"sv); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); @@ -1485,6 +1500,7 @@ void LevelGenData::init() DetourAttach((void**)&g_spawn_room_from_tile_codes_trampoline, spawn_room_from_tile_codes); DetourAttach((void**)&g_load_screen_trampoline, load_screen); + DetourAttach((void**)&g_unload_layer_trampoline, unload_layer); const LONG error = DetourTransactionCommit(); if (error != NO_ERROR) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 2a8c2fa96..cd3225e70 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -2142,3 +2142,25 @@ std::optional get_frametime_inactive() return memory_read(offset); return std::nullopt; } + +void destroy_layer(uint8_t layer) +{ + static size_t offset = 0; + if (offset == 0) + { + offset = get_address("unload_layer"); + } + if (offset != 0) + { + auto* layer_ptr = State::get().layer(layer); + typedef void destroy_func(Layer*); + static destroy_func* df = (destroy_func*)(offset); + df(layer_ptr); + } +} + +void destroy_level() +{ + destroy_layer(0); + destroy_layer(1); +} diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index fb2ab06ca..da32437db 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -150,3 +150,5 @@ void set_frametime(std::optional frametime); std::optional get_frametime(); void set_frametime_inactive(std::optional frametime); std::optional get_frametime_inactive(); +void destroy_layer(uint8_t layer); +void destroy_level(); diff --git a/src/game_api/script/events.cpp b/src/game_api/script/events.cpp index ff474b3a5..647cd429a 100644 --- a/src/game_api/script/events.cpp +++ b/src/game_api/script/events.cpp @@ -72,6 +72,29 @@ bool pre_load_screen() return block; } +bool pre_unload_level() +{ + bool block{false}; + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + block = backend->pre_unload_level(); + return !block; + }); + return block; +} +bool pre_unload_layer(LAYER layer) +{ + bool block{false}; + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + block = backend->pre_unload_layer(layer); + return !block; + }); + return block; +} + void post_room_generation() { LuaBackend::for_each_backend( @@ -99,6 +122,24 @@ void post_load_screen() return true; }); } +void post_unload_level() +{ + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + backend->post_unload_level(); + return true; + }); +} +void post_unload_layer(LAYER layer) +{ + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + backend->post_unload_layer(layer); + return true; + }); +} void on_death_message(STRINGID stringid) { LuaBackend::for_each_backend( diff --git a/src/game_api/script/events.hpp b/src/game_api/script/events.hpp index ad3e06581..0bade515e 100644 --- a/src/game_api/script/events.hpp +++ b/src/game_api/script/events.hpp @@ -18,9 +18,15 @@ struct Hud; void pre_load_level_files(); void pre_level_generation(); bool pre_load_screen(); +bool pre_unload_level(); +bool pre_unload_layer(LAYER layer); + void post_room_generation(); void post_level_generation(); void post_load_screen(); +void post_unload_level(); +void post_unload_layer(LAYER layer); + void on_death_message(STRINGID stringid); std::optional pre_get_feat(FEAT feat); bool pre_set_feat(FEAT feat); diff --git a/src/game_api/script/lua_backend.cpp b/src/game_api/script/lua_backend.cpp index f64752a9d..b38eb5039 100644 --- a/src/game_api/script/lua_backend.cpp +++ b/src/game_api/script/lua_backend.cpp @@ -865,6 +865,58 @@ bool LuaBackend::pre_load_screen() return false; } + +bool LuaBackend::pre_unload_level() +{ + if (!get_enabled()) + return false; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::PRE_UNLOAD_LEVEL) + { + set_current_callback(-1, id, CallbackType::Normal); + auto return_value = handle_function(this, callback.func).value_or(false); + clear_current_callback(); + callback.lastRan = now; + if (return_value) + return return_value; + } + } + + return false; +} +bool LuaBackend::pre_unload_layer(LAYER layer) +{ + if (!get_enabled()) + return false; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::PRE_UNLOAD_LAYER) + { + set_current_callback(-1, id, CallbackType::Normal); + auto return_value = handle_function(this, callback.func, layer).value_or(false); + clear_current_callback(); + callback.lastRan = now; + if (return_value) + return return_value; + } + } + + return false; +} + void LuaBackend::post_room_generation() { if (!get_enabled()) @@ -983,6 +1035,49 @@ void LuaBackend::post_load_screen() } } } +void LuaBackend::post_unload_level() +{ + if (!get_enabled()) + return; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::POST_UNLOAD_LEVEL) + { + set_current_callback(-1, id, CallbackType::Normal); + handle_function(this, callback.func); + clear_current_callback(); + callback.lastRan = now; + } + } +} +void LuaBackend::post_unload_layer(LAYER layer) +{ + if (!get_enabled()) + return; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::POST_UNLOAD_LAYER) + { + set_current_callback(-1, id, CallbackType::Normal); + handle_function(this, callback.func, layer); + clear_current_callback(); + callback.lastRan = now; + } + } +} + void LuaBackend::on_death_message(STRINGID stringid) { if (!get_enabled()) diff --git a/src/game_api/script/lua_backend.hpp b/src/game_api/script/lua_backend.hpp index 15e5ed89e..bc627c3db 100644 --- a/src/game_api/script/lua_backend.hpp +++ b/src/game_api/script/lua_backend.hpp @@ -119,7 +119,11 @@ enum class ON PRE_SET_FEAT, PRE_UPDATE, POST_UPDATE, - USER_DATA + USER_DATA, + PRE_UNLOAD_LEVEL, + POST_UNLOAD_LEVEL, + PRE_UNLOAD_LAYER, + POST_UNLOAD_LAYER, }; struct IntOption @@ -362,9 +366,15 @@ class LuaBackend void pre_load_level_files(); void pre_level_generation(); bool pre_load_screen(); + bool pre_unload_level(); + bool pre_unload_layer(LAYER layer); + void post_room_generation(); void post_level_generation(); void post_load_screen(); + void post_unload_level(); + void post_unload_layer(LAYER layer); + void on_death_message(STRINGID stringid); std::optional pre_get_feat(FEAT feat); bool pre_set_feat(FEAT feat); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 7930f2404..4bd15d1fd 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2094,6 +2094,12 @@ end /// Get engine target frametime when game is unfocused (1/framerate, default 1/33). lua["get_frametime_unfocused"] = get_frametime_inactive; + /// Destroys all layers and all entities in the level. Probably a bad idea. + lua["destroy_level"] = destroy_level; + + /// Destroys a layer and all entities in it. No idea where you would need this either. + lua["destroy_layer"] = destroy_layer; + lua.create_named_table("INPUTS", "NONE", 0, "JUMP", 1, "WHIP", 2, "BOMB", 4, "ROPE", 8, "RUN", 16, "DOOR", 32, "MENU", 64, "JOURNAL", 128, "LEFT", 256, "RIGHT", 512, "UP", 1024, "DOWN", 2048); lua.create_named_table( @@ -2248,7 +2254,15 @@ end "POST_UPDATE", ON::POST_UPDATE, "USER_DATA", - ON::USER_DATA); + ON::USER_DATA, + "PRE_UNLOAD_LEVEL", + ON::PRE_UNLOAD_LEVEL, + "POST_UNLOAD_LEVEL", + ON::POST_UNLOAD_LEVEL, + "PRE_UNLOAD_LAYER", + ON::PRE_UNLOAD_LAYER, + "POST_UNLOAD_LAYER", + ON::POST_UNLOAD_LAYER); /* ON // LOGO // Runs when entering the the mossmouth logo screen. @@ -2460,6 +2474,16 @@ end // USER_DATA // Params: Entity ent // Runs on all changes to Entity.user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels. + // PRE_UNLOAD_LEVEL + // Runs right before the current level is unloaded and any entities destroyed + // POST_UNLOAD_LEVEL + // Runs right after the current level has been unloaded and all entities destroyed + // PRE_UNLOAD_LAYER + // Params: LAYER layer + // Runs right before a layer is unloaded and any entities there destroyed + // POST_UNLOAD_LAYER + // Params: LAYER layer + // Runs right after a layer has been unloaded and any entities there destroyed */ lua.create_named_table( diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index f8c8e0a02..1ef6e0da1 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2034,6 +2034,15 @@ std::unordered_map g_address_rules{ .offset(0x21) .at_exe(), }, + { + "unload_layer"sv, + // bp on destroy entity, leave level, it's third in stack or something + PatternCommandBuffer{} + .find_inst("49 89 cc 8b 41 18 85 c0 74 0b 49 8b 74 24 08"_gh) + .at_exe() + .function_start(), + }, + }; std::unordered_map g_cached_addresses; From eeeeabff450f1d3dda9682b1a055c19620b550ae Mon Sep 17 00:00:00 2001 From: Dregu Date: Wed, 11 Oct 2023 18:07:43 +0300 Subject: [PATCH 06/29] rename unload_level and add pre_spawn_level --- src/game_api/script/events.cpp | 14 +++++++++++ src/game_api/script/lua_backend.cpp | 30 +++++++++++++++++++--- src/game_api/script/lua_backend.hpp | 10 +++++--- src/game_api/script/lua_vm.cpp | 39 ++++++++++++++++------------- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/game_api/script/events.cpp b/src/game_api/script/events.cpp index 647cd429a..11d2b5a4c 100644 --- a/src/game_api/script/events.cpp +++ b/src/game_api/script/events.cpp @@ -16,6 +16,8 @@ class JournalPage; struct AABB; +auto g_level_loaded = false; + void pre_load_level_files() { LuaBackend::for_each_backend( @@ -74,6 +76,7 @@ bool pre_load_screen() } bool pre_unload_level() { + g_level_loaded = false; bool block{false}; LuaBackend::for_each_backend( [&](LuaBackend::LockedBackend backend) @@ -207,6 +210,17 @@ void post_tile_code_spawn(std::string_view tile_code, float x, float y, int laye Entity* pre_entity_spawn(std::uint32_t entity_type, float x, float y, int layer, Entity* overlay, int spawn_type_flags) { + if (!g_level_loaded) + { + g_level_loaded = true; + LuaBackend::for_each_backend( + [=](LuaBackend::LockedBackend backend) + { + backend->pre_spawn(); + return true; + }); + } + Entity* spawned_ent{nullptr}; LuaBackend::for_each_backend( [=, &spawned_ent](LuaBackend::LockedBackend backend) diff --git a/src/game_api/script/lua_backend.cpp b/src/game_api/script/lua_backend.cpp index b38eb5039..350ab34b1 100644 --- a/src/game_api/script/lua_backend.cpp +++ b/src/game_api/script/lua_backend.cpp @@ -878,7 +878,7 @@ bool LuaBackend::pre_unload_level() if (is_callback_cleared(id)) continue; - if (callback.screen == ON::PRE_UNLOAD_LEVEL) + if (callback.screen == ON::PRE_LEVEL_DESTRUCTION) { set_current_callback(-1, id, CallbackType::Normal); auto return_value = handle_function(this, callback.func).value_or(false); @@ -903,7 +903,7 @@ bool LuaBackend::pre_unload_layer(LAYER layer) if (is_callback_cleared(id)) continue; - if (callback.screen == ON::PRE_UNLOAD_LAYER) + if (callback.screen == ON::PRE_LAYER_DESTRUCTION) { set_current_callback(-1, id, CallbackType::Normal); auto return_value = handle_function(this, callback.func, layer).value_or(false); @@ -1047,7 +1047,7 @@ void LuaBackend::post_unload_level() if (is_callback_cleared(id)) continue; - if (callback.screen == ON::POST_UNLOAD_LEVEL) + if (callback.screen == ON::POST_LEVEL_DESTRUCTION) { set_current_callback(-1, id, CallbackType::Normal); handle_function(this, callback.func); @@ -1068,7 +1068,7 @@ void LuaBackend::post_unload_layer(LAYER layer) if (is_callback_cleared(id)) continue; - if (callback.screen == ON::POST_UNLOAD_LAYER) + if (callback.screen == ON::POST_LAYER_DESTRUCTION) { set_current_callback(-1, id, CallbackType::Normal); handle_function(this, callback.func, layer); @@ -1209,6 +1209,28 @@ void LuaBackend::post_entity_spawn(Entity* entity, int spawn_type_flags) } } +void LuaBackend::pre_spawn() +{ + if (!get_enabled()) + return; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::PRE_LEVEL_SPAWN) + { + set_current_callback(-1, id, CallbackType::Normal); + handle_function(this, callback.func); + clear_current_callback(); + callback.lastRan = now; + } + } +} + bool LuaBackend::pre_entity_instagib(Entity* victim) { bool skip{false}; diff --git a/src/game_api/script/lua_backend.hpp b/src/game_api/script/lua_backend.hpp index bc627c3db..b7e9c0716 100644 --- a/src/game_api/script/lua_backend.hpp +++ b/src/game_api/script/lua_backend.hpp @@ -120,10 +120,11 @@ enum class ON PRE_UPDATE, POST_UPDATE, USER_DATA, - PRE_UNLOAD_LEVEL, - POST_UNLOAD_LEVEL, - PRE_UNLOAD_LAYER, - POST_UNLOAD_LAYER, + PRE_LEVEL_DESTRUCTION, + POST_LEVEL_DESTRUCTION, + PRE_LAYER_DESTRUCTION, + POST_LAYER_DESTRUCTION, + PRE_LEVEL_SPAWN, }; struct IntOption @@ -389,6 +390,7 @@ class LuaBackend Entity* pre_entity_spawn(std::uint32_t entity_type, float x, float y, int layer, Entity* overlay, int spawn_type_flags); void post_entity_spawn(Entity* entity, int spawn_type_flags); + void pre_spawn(); bool pre_entity_instagib(Entity* victim); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 4bd15d1fd..aa4a5de44 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2255,14 +2255,17 @@ end ON::POST_UPDATE, "USER_DATA", ON::USER_DATA, - "PRE_UNLOAD_LEVEL", - ON::PRE_UNLOAD_LEVEL, - "POST_UNLOAD_LEVEL", - ON::POST_UNLOAD_LEVEL, - "PRE_UNLOAD_LAYER", - ON::PRE_UNLOAD_LAYER, - "POST_UNLOAD_LAYER", - ON::POST_UNLOAD_LAYER); + "PRE_LEVEL_DESTRUCTION", + ON::PRE_LEVEL_DESTRUCTION, + "POST_LEVEL_DESTRUCTION", + ON::POST_LEVEL_DESTRUCTION, + "PRE_LAYER_DESTRUCTION", + ON::PRE_LAYER_DESTRUCTION, + "POST_LAYER_DESTRUCTION", + ON::POST_LAYER_DESTRUCTION, + "PRE_LEVEL_SPAWN", + ON::PRE_LEVEL_SPAWN); + /* ON // LOGO // Runs when entering the the mossmouth logo screen. @@ -2339,7 +2342,7 @@ end // Params: PreLoadLevelFilesContext load_level_ctx // Runs right before level files would be loaded // PRE_LEVEL_GENERATION - // Runs before any level generation, no entities should exist at this point + // Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens, see PRE_LEVEL_SPAWN. // POST_ROOM_GENERATION // Params: PostRoomGenerationContext room_gen_ctx // Runs right after all rooms are generated before entities are spawned @@ -2474,16 +2477,18 @@ end // USER_DATA // Params: Entity ent // Runs on all changes to Entity.user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels. - // PRE_UNLOAD_LEVEL - // Runs right before the current level is unloaded and any entities destroyed - // POST_UNLOAD_LEVEL - // Runs right after the current level has been unloaded and all entities destroyed - // PRE_UNLOAD_LAYER + // PRE_LEVEL_DESTRUCTION + // Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last. + // POST_LEVEL_DESTRUCTION + // Runs right after the current level has been unloaded and all entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last. + // PRE_LAYER_DESTRUCTION // Params: LAYER layer - // Runs right before a layer is unloaded and any entities there destroyed - // POST_UNLOAD_LAYER + // Runs right before a layer is unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last. + // POST_LAYER_DESTRUCTION // Params: LAYER layer - // Runs right after a layer has been unloaded and any entities there destroyed + // Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last. + // PRE_LEVEL_SPAWN + // Runs right before the first entity in any screen is spawned. Doesn't run if the screen doesn't spawn entities. You should probably prefer PRE_LEVEL_GENERATION, but that doesn't support all level-like screens. */ lua.create_named_table( From fdfd1adc1a29144a4fe124287b9b04121e10b5ec Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 00:12:57 +0300 Subject: [PATCH 07/29] layer init hooks --- src/game_api/level_api.cpp | 18 ++++- src/game_api/rpc.cpp | 22 +++++ src/game_api/rpc.hpp | 2 + src/game_api/script/events.cpp | 64 +++++++++++---- src/game_api/script/events.hpp | 6 +- src/game_api/script/lua_backend.cpp | 121 ++++++++++++++++++++++------ src/game_api/script/lua_backend.hpp | 12 ++- src/game_api/script/lua_vm.cpp | 36 +++++++-- src/game_api/search.cpp | 7 +- 9 files changed, 234 insertions(+), 54 deletions(-) diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index dd3cf3e6f..526a9e1d1 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -826,7 +826,8 @@ void level_gen(LevelGenSystem* level_gen_sys, float param_2, size_t param_3) g_CustomShopTypes[0] = {}; g_CustomShopTypes[1] = {}; - pre_level_generation(); + if (pre_level_generation()) + return; g_level_gen_trampoline(level_gen_sys, param_2, param_3); post_level_generation(); @@ -870,6 +871,19 @@ void unload_layer(Layer* layer) post_unload_level(); } +using InitLayerFun = void(Layer*); +InitLayerFun* g_init_layer_trampoline{nullptr}; +void load_layer(Layer* layer) +{ + if (!layer->is_back_layer) + pre_init_level(); + pre_init_layer((LAYER)layer->is_back_layer); + g_init_layer_trampoline(layer); + post_init_layer((LAYER)layer->is_back_layer); + if (layer->is_back_layer) + post_init_level(); +} + using HandleTileCodeFun = void(LevelGenSystem*, std::uint32_t, std::uint64_t, float, float, std::uint8_t); HandleTileCodeFun* g_handle_tile_code_trampoline{nullptr}; void handle_tile_code(LevelGenSystem* self, std::uint32_t tile_code, std::uint16_t room_template, float x, float y, std::uint8_t layer) @@ -1485,6 +1499,7 @@ void LevelGenData::init() g_load_screen_trampoline = (LoadScreenFun*)get_address("load_screen_func"sv); g_unload_layer_trampoline = (UnloadLayerFun*)get_address("unload_layer"sv); + g_init_layer_trampoline = (InitLayerFun*)get_address("init_layer"sv); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); @@ -1501,6 +1516,7 @@ void LevelGenData::init() DetourAttach((void**)&g_load_screen_trampoline, load_screen); DetourAttach((void**)&g_unload_layer_trampoline, unload_layer); + DetourAttach((void**)&g_init_layer_trampoline, load_layer); const LONG error = DetourTransactionCommit(); if (error != NO_ERROR) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index cd3225e70..6b3c6c4bb 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -2164,3 +2164,25 @@ void destroy_level() destroy_layer(0); destroy_layer(1); } + +void create_layer(uint8_t layer) +{ + static size_t offset = 0; + if (offset == 0) + { + offset = get_address("init_layer"); + } + if (offset != 0) + { + auto* layer_ptr = State::get().layer(layer); + typedef void init_func(Layer*); + static init_func* ilf = (init_func*)(offset); + ilf(layer_ptr); + } +} + +void create_level() +{ + create_layer(0); + create_layer(1); +} diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index da32437db..2c4b60465 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -152,3 +152,5 @@ void set_frametime_inactive(std::optional frametime); std::optional get_frametime_inactive(); void destroy_layer(uint8_t layer); void destroy_level(); +void create_layer(uint8_t layer); +void create_level(); diff --git a/src/game_api/script/events.cpp b/src/game_api/script/events.cpp index 11d2b5a4c..13bc37131 100644 --- a/src/game_api/script/events.cpp +++ b/src/game_api/script/events.cpp @@ -27,14 +27,16 @@ void pre_load_level_files() return true; }); } -void pre_level_generation() +bool pre_level_generation() { + bool block{false}; LuaBackend::for_each_backend( [&](LuaBackend::LockedBackend backend) { - backend->pre_level_generation(); - return true; + block = backend->pre_level_generation(); + return !block; }); + return block; } bool pre_load_screen() { @@ -76,7 +78,6 @@ bool pre_load_screen() } bool pre_unload_level() { - g_level_loaded = false; bool block{false}; LuaBackend::for_each_backend( [&](LuaBackend::LockedBackend backend) @@ -84,6 +85,21 @@ bool pre_unload_level() block = backend->pre_unload_level(); return !block; }); + if (!block) + g_level_loaded = false; + return block; +} +bool pre_init_level() +{ + bool block{false}; + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + block = backend->pre_init_level(); + return !block; + }); + if (!block) + g_level_loaded = true; return block; } bool pre_unload_layer(LAYER layer) @@ -97,6 +113,17 @@ bool pre_unload_layer(LAYER layer) }); return block; } +bool pre_init_layer(LAYER layer) +{ + bool block{false}; + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + block = backend->pre_init_layer(layer); + return !block; + }); + return block; +} void post_room_generation() { @@ -116,6 +143,24 @@ void post_level_generation() return true; }); } +void post_init_layer(LAYER layer) +{ + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + backend->post_init_layer(layer); + return true; + }); +} +void post_init_level() +{ + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + backend->post_init_level(); + return true; + }); +} void post_load_screen() { LuaBackend::for_each_backend( @@ -210,17 +255,6 @@ void post_tile_code_spawn(std::string_view tile_code, float x, float y, int laye Entity* pre_entity_spawn(std::uint32_t entity_type, float x, float y, int layer, Entity* overlay, int spawn_type_flags) { - if (!g_level_loaded) - { - g_level_loaded = true; - LuaBackend::for_each_backend( - [=](LuaBackend::LockedBackend backend) - { - backend->pre_spawn(); - return true; - }); - } - Entity* spawned_ent{nullptr}; LuaBackend::for_each_backend( [=, &spawned_ent](LuaBackend::LockedBackend backend) diff --git a/src/game_api/script/events.hpp b/src/game_api/script/events.hpp index 0bade515e..fff659bcd 100644 --- a/src/game_api/script/events.hpp +++ b/src/game_api/script/events.hpp @@ -16,14 +16,18 @@ struct HudData; struct Hud; void pre_load_level_files(); -void pre_level_generation(); +bool pre_level_generation(); bool pre_load_screen(); +bool pre_init_level(); +bool pre_init_layer(LAYER layer); bool pre_unload_level(); bool pre_unload_layer(LAYER layer); void post_room_generation(); void post_level_generation(); void post_load_screen(); +void post_init_level(); +void post_init_layer(LAYER layer); void post_unload_level(); void post_unload_layer(LAYER layer); diff --git a/src/game_api/script/lua_backend.cpp b/src/game_api/script/lua_backend.cpp index 350ab34b1..3aee0fa04 100644 --- a/src/game_api/script/lua_backend.cpp +++ b/src/game_api/script/lua_backend.cpp @@ -751,10 +751,10 @@ void LuaBackend::pre_load_level_files() } } } -void LuaBackend::pre_level_generation() +bool LuaBackend::pre_level_generation() { if (!get_enabled()) - return; + return false; auto now = get_frame_count(); @@ -766,11 +766,62 @@ void LuaBackend::pre_level_generation() if (callback.screen == ON::PRE_LEVEL_GENERATION) { set_current_callback(-1, id, CallbackType::Normal); - handle_function(this, callback.func); + auto return_value = handle_function(this, callback.func).value_or(false); clear_current_callback(); callback.lastRan = now; + if (return_value) + return return_value; } } + return false; +} +bool LuaBackend::pre_init_level() +{ + if (!get_enabled()) + return false; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::PRE_LEVEL_CREATION) + { + set_current_callback(-1, id, CallbackType::Normal); + auto return_value = handle_function(this, callback.func).value_or(false); + clear_current_callback(); + callback.lastRan = now; + if (return_value) + return return_value; + } + } + return false; +} +bool LuaBackend::pre_init_layer(LAYER layer) +{ + if (!get_enabled()) + return false; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::PRE_LAYER_CREATION) + { + set_current_callback(-1, id, CallbackType::Normal); + auto return_value = handle_function(this, callback.func, layer).value_or(false); + clear_current_callback(); + callback.lastRan = now; + if (return_value) + return return_value; + } + } + return false; } bool LuaBackend::pre_load_screen() { @@ -1008,6 +1059,48 @@ void LuaBackend::post_level_generation() } } } +void LuaBackend::post_init_level() +{ + if (!get_enabled()) + return; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::POST_LEVEL_CREATION) + { + set_current_callback(-1, id, CallbackType::Normal); + handle_function(this, callback.func); + clear_current_callback(); + callback.lastRan = now; + } + } +} +void LuaBackend::post_init_layer(LAYER layer) +{ + if (!get_enabled()) + return; + + auto now = get_frame_count(); + + for (auto& [id, callback] : callbacks) + { + if (is_callback_cleared(id)) + continue; + + if (callback.screen == ON::POST_LAYER_CREATION) + { + set_current_callback(-1, id, CallbackType::Normal); + handle_function(this, callback.func, layer); + clear_current_callback(); + callback.lastRan = now; + } + } +} void LuaBackend::post_load_screen() { if (!get_enabled()) @@ -1209,28 +1302,6 @@ void LuaBackend::post_entity_spawn(Entity* entity, int spawn_type_flags) } } -void LuaBackend::pre_spawn() -{ - if (!get_enabled()) - return; - - auto now = get_frame_count(); - - for (auto& [id, callback] : callbacks) - { - if (is_callback_cleared(id)) - continue; - - if (callback.screen == ON::PRE_LEVEL_SPAWN) - { - set_current_callback(-1, id, CallbackType::Normal); - handle_function(this, callback.func); - clear_current_callback(); - callback.lastRan = now; - } - } -} - bool LuaBackend::pre_entity_instagib(Entity* victim) { bool skip{false}; diff --git a/src/game_api/script/lua_backend.hpp b/src/game_api/script/lua_backend.hpp index b7e9c0716..835cd8168 100644 --- a/src/game_api/script/lua_backend.hpp +++ b/src/game_api/script/lua_backend.hpp @@ -120,11 +120,14 @@ enum class ON PRE_UPDATE, POST_UPDATE, USER_DATA, + PRE_LEVEL_CREATION, + POST_LEVEL_CREATION, + PRE_LAYER_CREATION, + POST_LAYER_CREATION, PRE_LEVEL_DESTRUCTION, POST_LEVEL_DESTRUCTION, PRE_LAYER_DESTRUCTION, POST_LAYER_DESTRUCTION, - PRE_LEVEL_SPAWN, }; struct IntOption @@ -365,14 +368,18 @@ class LuaBackend void post_tile_code(std::string_view tile_code, float x, float y, int layer, uint16_t room_template); void pre_load_level_files(); - void pre_level_generation(); + bool pre_level_generation(); bool pre_load_screen(); + bool pre_init_level(); + bool pre_init_layer(LAYER layer); bool pre_unload_level(); bool pre_unload_layer(LAYER layer); void post_room_generation(); void post_level_generation(); void post_load_screen(); + void post_init_level(); + void post_init_layer(LAYER layer); void post_unload_level(); void post_unload_layer(LAYER layer); @@ -390,7 +397,6 @@ class LuaBackend Entity* pre_entity_spawn(std::uint32_t entity_type, float x, float y, int layer, Entity* overlay, int spawn_type_flags); void post_entity_spawn(Entity* entity, int spawn_type_flags); - void pre_spawn(); bool pre_entity_instagib(Entity* victim); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index aa4a5de44..0a57faa2c 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2094,12 +2094,18 @@ end /// Get engine target frametime when game is unfocused (1/framerate, default 1/33). lua["get_frametime_unfocused"] = get_frametime_inactive; - /// Destroys all layers and all entities in the level. Probably a bad idea. + /// Destroys all layers and all entities in the level. Usually a bad idea, unless you also call create_level and spawn the player back in. lua["destroy_level"] = destroy_level; - /// Destroys a layer and all entities in it. No idea where you would need this either. + /// Destroys a layer and all entities in it. lua["destroy_layer"] = destroy_layer; + /// Initializes an empty front and back layer that don't currently exist. Does nothing(?) if layers already exist. + lua["create_level"] = create_level; + + /// Initializes an empty layer that doesn't currently exist. + lua["create_layer"] = create_layer; + lua.create_named_table("INPUTS", "NONE", 0, "JUMP", 1, "WHIP", 2, "BOMB", 4, "ROPE", 8, "RUN", 16, "DOOR", 32, "MENU", 64, "JOURNAL", 128, "LEFT", 256, "RIGHT", 512, "UP", 1024, "DOWN", 2048); lua.create_named_table( @@ -2255,6 +2261,14 @@ end ON::POST_UPDATE, "USER_DATA", ON::USER_DATA, + "PRE_LEVEL_CREATION", + ON::PRE_LEVEL_CREATION, + "POST_LEVEL_CREATION", + ON::POST_LEVEL_CREATION, + "PRE_LAYER_CREATION", + ON::PRE_LAYER_CREATION, + "POST_LAYER_CREATION", + ON::POST_LAYER_CREATION, "PRE_LEVEL_DESTRUCTION", ON::PRE_LEVEL_DESTRUCTION, "POST_LEVEL_DESTRUCTION", @@ -2262,9 +2276,7 @@ end "PRE_LAYER_DESTRUCTION", ON::PRE_LAYER_DESTRUCTION, "POST_LAYER_DESTRUCTION", - ON::POST_LAYER_DESTRUCTION, - "PRE_LEVEL_SPAWN", - ON::PRE_LEVEL_SPAWN); + ON::POST_LAYER_DESTRUCTION); /* ON // LOGO @@ -2342,7 +2354,7 @@ end // Params: PreLoadLevelFilesContext load_level_ctx // Runs right before level files would be loaded // PRE_LEVEL_GENERATION - // Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens, see PRE_LEVEL_SPAWN. + // Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens. Return true to stop normal level generation. // POST_ROOM_GENERATION // Params: PostRoomGenerationContext room_gen_ctx // Runs right after all rooms are generated before entities are spawned @@ -2477,6 +2489,16 @@ end // USER_DATA // Params: Entity ent // Runs on all changes to Entity.user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels. + // PRE_LEVEL_CREATION + // Runs right before the front layer is created. Runs in all screens that usually have entities, or when creating a layer manually. + // POST_LEVEL_CREATION + // Runs right after the back layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually. + // PRE_LAYER_CREATION + // Params: LAYER layer + // Runs right before a layer is created. Runs in all screens that usually have entities, or when creating a layer manually. + // POST_LAYER_CREATION + // Params: LAYER layer + // Runs right after a layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually. // PRE_LEVEL_DESTRUCTION // Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last. // POST_LEVEL_DESTRUCTION @@ -2487,8 +2509,6 @@ end // POST_LAYER_DESTRUCTION // Params: LAYER layer // Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last. - // PRE_LEVEL_SPAWN - // Runs right before the first entity in any screen is spawned. Doesn't run if the screen doesn't spawn entities. You should probably prefer PRE_LEVEL_GENERATION, but that doesn't support all level-like screens. */ lua.create_named_table( diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 1ef6e0da1..3a4a67d9f 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2042,7 +2042,12 @@ std::unordered_map g_address_rules{ .at_exe() .function_start(), }, - + { + "init_layer"sv, + // called a lot from the load_screen function + PatternCommandBuffer{} + .from_exe_base(0x228b58f0), + }, }; std::unordered_map g_cached_addresses; From 0c3b6c7083d78babc34a0d18075c2e2fe3c98afb Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 01:43:20 +0300 Subject: [PATCH 08/29] add set_death_enabled --- src/game_api/rpc.cpp | 16 ++++++++++++++++ src/game_api/rpc.hpp | 1 + src/game_api/script/lua_vm.cpp | 3 +++ src/game_api/search.cpp | 8 +++++++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 6b3c6c4bb..bde6b9124 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -2186,3 +2186,19 @@ void create_level() create_layer(0); create_layer(1); } + +void set_death_enabled(bool enable) +{ + static size_t offset = 0; + if (offset == 0) + { + offset = get_address("dead_players"); + } + if (offset != 0) + { + if (!enable) + write_mem_recoverable("death_disable", offset, "\xC3\x90"sv, true); + else + recover_mem("death_disable"); + } +} diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index 2c4b60465..92d9690d5 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -154,3 +154,4 @@ void destroy_layer(uint8_t layer); void destroy_level(); void create_layer(uint8_t layer); void create_level(); +void set_death_enabled(bool enable); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 0a57faa2c..48b0c9c3e 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2106,6 +2106,9 @@ end /// Initializes an empty layer that doesn't currently exist. lua["create_layer"] = create_layer; + /// Setting to false disables the death screen from popping up for any usual reason, can still load manually + lua["set_death_enabled"] = set_death_enabled; + lua.create_named_table("INPUTS", "NONE", 0, "JUMP", 1, "WHIP", 2, "BOMB", 4, "ROPE", 8, "RUN", 16, "DOOR", 32, "MENU", 64, "JOURNAL", 128, "LEFT", 256, "RIGHT", 512, "UP", 1024, "DOWN", 2048); lua.create_named_table( diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 3a4a67d9f..6e0acbc12 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2044,10 +2044,16 @@ std::unordered_map g_address_rules{ }, { "init_layer"sv, - // called a lot from the load_screen function + // TODO PatternCommandBuffer{} .from_exe_base(0x228b58f0), }, + { + "dead_players"sv, + // TODO + PatternCommandBuffer{} + .from_exe_base(0x22c061d0), + }, }; std::unordered_map g_cached_addresses; From 28337d44d3f16b22065b7fd056f8ba95103d411b Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 02:29:17 +0300 Subject: [PATCH 09/29] add ui options to disable death and respawn players --- src/injected/ui.cpp | 52 +++++++++++++++++++++++++++++++++++++++- src/injected/ui_util.cpp | 4 ++++ src/injected/ui_util.hpp | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 318d8ebc9..5c66d07d9 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -213,6 +213,7 @@ std::map default_keys{ {"speedhack_turbo", VK_PRIOR}, {"speedhack_slow", VK_NEXT}, {"toggle_uncapped_fps", OL_KEY_CTRL | OL_KEY_SHIFT | 'U'}, + {"respawn", OL_KEY_CTRL | 'R'}, //{ "", 0x }, }; @@ -267,7 +268,7 @@ std::vector g_selected_ids; bool set_focus_entity = false, set_focus_world = false, set_focus_zoom = false, set_focus_finder = false, set_focus_uid = false, scroll_to_entity = false, scroll_top = false, click_teleport = false, throw_held = false, paused = 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, pause_updates = true; + 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, pause_updates = true, 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; @@ -2290,6 +2291,44 @@ void warp_next_level(size_t num) } } +void respawn() +{ + if (g_state->screen != 11 && g_state->screen != 12) + { + if (g_state->screen > 11) + { + quick_start(12, g_state->world_start, g_state->level_start, g_state->theme_start); + } + else + { + quick_start(12, 1, 1, 1); + } + return; + } + for (int8_t i = 0; i < 4; ++i) + { + auto found = false; + for (auto p : UI::get_players()) + { + if (p->inventory_ptr->player_slot == i) + { + found = true; + if (p->health == 0 || test_flag(p->flags, 29)) + { + p->health = 4; + p->flags = clr_flag(p->flags, 29); + p->set_behavior(1); + } + } + } + if (!found) + { + g_state->items->player_inventories[i].health = 4; + UI::spawn_player(i, g_state->level_gen->spawn_x, g_state->level_gen->spawn_y); + } + } +} + bool pressed(std::string keyname, WPARAM wParam) { if (keys.find(keyname) == keys.end() || (keys[keyname] & 0xff) == 0) @@ -2853,6 +2892,10 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) g_engine_fps = 0; update_frametimes(); } + else if (pressed("respawn", wParam)) + { + respawn(); + } else if (pressed("toggle_godmode", wParam)) { options["god_mode"] = !options["god_mode"]; @@ -5524,6 +5567,11 @@ void render_options() UI::godmode_companions(options["god_mode_companions"]); } tooltip("Make the hired hands completely deathproof."); + if (ImGui::Checkbox("Disable death screen##NoDeath", &death_disable)) + { + UI::death_enabled(!death_disable); + } + tooltip("Disable the death screen from popping up for any reason."); if (ImGui::Checkbox("Noclip##Noclip", &options["noclip"])) { toggle_noclip(); @@ -7988,6 +8036,8 @@ void render_game_props() } if (submenu("Players")) { + if (ImGui::MenuItem("Respawn dead players")) + respawn(); ImGui::TextWrapped("New players spawned here can't be controlled, but can be used to test some things that require multiple players."); if (ImGui::SliderScalar("Number of players##SetNumPlayers", ImGuiDataType_U8, &g_state->items->player_count, &u8_one, &u8_four, "%d", ImGuiSliderFlags_AlwaysClamp)) { diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index 95ae45c26..f76e1df19 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -35,6 +35,10 @@ void UI::godmode_companions(bool g) { State::get().godmode_companions(g); } +void UI::death_enabled(bool g) +{ + set_death_enabled(g); +} std::pair UI::click_position(float x, float y) { return State::click_position(x, y); diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp index 4fd364e2a..d2325d4cd 100644 --- a/src/injected/ui_util.hpp +++ b/src/injected/ui_util.hpp @@ -37,6 +37,7 @@ class UI public: static void godmode(bool g); static void godmode_companions(bool g); + static void death_enabled(bool g); static std::pair click_position(float x, float y); static void zoom(float level); static uint32_t get_frame_count(); From 0e3294ea6d5fd419e387c0515a9e0df2bb0cb628 Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 18:19:30 +0300 Subject: [PATCH 10/29] fix get_procedural_chance logic --- src/game_api/level_api.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index 526a9e1d1..43c54f129 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -1989,14 +1989,18 @@ uint32_t LevelGenSystem::get_procedural_spawn_chance(uint32_t chance_id) if (!this_chances.chances.empty()) { auto* state = State::get().ptr(); - if (this_chances.chances.size() >= state->level) + if (this_chances.chances.size() >= state->level && state->level > 0) { - return this_chances.chances[state->level]; + return this_chances.chances[state->level - 1]; } - else + else if (this_chances.chances.size() == 1) { return this_chances.chances[0]; } + else + { + return 0; + } } } @@ -2006,14 +2010,18 @@ uint32_t LevelGenSystem::get_procedural_spawn_chance(uint32_t chance_id) if (!this_chances.chances.empty()) { auto* state = State::get().ptr(); - if (this_chances.chances.size() >= state->level) + if (this_chances.chances.size() >= state->level && state->level > 0) { - return this_chances.chances[state->level]; + return this_chances.chances[state->level - 1]; } - else + else if (this_chances.chances.size() == 1) { return this_chances.chances[0]; } + else + { + return 0; + } } } From c19bb9df2d50d79f0ed352c6bde31db55f25bca2 Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 19:53:42 +0300 Subject: [PATCH 11/29] add procedural chances to ui --- src/game_api/flags.hpp | 18 ++++++++++++++++++ src/game_api/level_api.cpp | 9 +++++++++ src/game_api/level_api.hpp | 2 +- src/injected/ui.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/game_api/flags.hpp b/src/game_api/flags.hpp index 7d01b0f30..60cbf73e2 100644 --- a/src/game_api/flags.hpp +++ b/src/game_api/flags.hpp @@ -789,3 +789,21 @@ const char* levelgen_flags3[]{ "7: unknown", "8: unknown", }; + +const char* level_chances[]{ + "backroom", + "backroom interconnect", + "backroom hidden door", + "backroom hidden cache", + "mount", + "altar", + "idol", + "floor side spread", + "floor bottom spread", + "background", + "ground background", + "bigroom", + "wideroom", + "tallroom", + "rewardroom", +}; diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index 43c54f129..8ad4a15d1 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -1981,6 +1981,15 @@ LevelChanceDef& get_or_emplace_level_chance(game_unordered_mapvalue.second; } +std::optional LevelGenSystem::get_procedural_spawn_chance_name(uint32_t chance_id) +{ + if (g_monster_chance_id_to_name.contains(chance_id)) + return g_monster_chance_id_to_name[chance_id]; + if (g_trap_chance_id_to_name.contains(chance_id)) + return g_trap_chance_id_to_name[chance_id]; + return std::nullopt; +} + uint32_t LevelGenSystem::get_procedural_spawn_chance(uint32_t chance_id) { if (g_monster_chance_id_to_name.contains(chance_id)) diff --git a/src/game_api/level_api.hpp b/src/game_api/level_api.hpp index cd6d900b3..88b444911 100644 --- a/src/game_api/level_api.hpp +++ b/src/game_api/level_api.hpp @@ -531,7 +531,7 @@ struct LevelGenSystem bool set_shop_type(uint32_t x, uint32_t y, uint8_t l, ShopType shop_type); std::string_view get_room_template_name(uint16_t room_template); - + std::optional get_procedural_spawn_chance_name(uint32_t chance_id); uint32_t get_procedural_spawn_chance(uint32_t chance_id); bool set_procedural_spawn_chance(uint32_t chance_id, uint32_t inverse_chance); diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 5c66d07d9..b3cc1d300 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -8149,6 +8149,44 @@ void render_game_props() endmenu(); } + if (submenu("Procedural chances")) + { + static auto render_procedural_chance = [](uint32_t id, LevelChanceDef& def) + { + int inverse_chance = g_state->level_gen->get_procedural_spawn_chance(id); + std::string name = std::string(g_state->level_gen->get_procedural_spawn_chance_name(id).value_or(fmt::format("{}", id))); + if (def.chances.empty()) + return; + float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0; + std::string all = fmt::format("{}", def.chances[0]); + for (auto i = 1; i < def.chances.size(); ++i) + all += "," + fmt::format("{}", def.chances[i]); + std::string str = fmt::format("{:.3f}% ({})", chance, all); + ImGui::LabelText(str.c_str(), name.c_str()); + }; + + static auto render_chance = [](int inverse_chance, const char* name) + { + float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0; + std::string str = fmt::format("{:.3f}%", chance); + ImGui::LabelText(str.c_str(), name); + }; + + ImGui::SeparatorText("Monster chances"); + for (auto [id, def] : g_state->level_gen->data->level_monster_chances) + render_procedural_chance(id, def); + + ImGui::SeparatorText("Trap chances"); + for (auto [id, def] : g_state->level_gen->data->level_trap_chances) + render_procedural_chance(id, def); + + ImGui::SeparatorText("Level chances"); + if (g_state->current_theme) + render_chance(g_state->current_theme->get_shop_chance(), "shop"); + for (auto i = 0; i < 15; ++i) + render_chance(g_state->level_gen->data->level_config[i], level_chances[i]); + endmenu(); + } if (submenu("AI targets")) { for (size_t x = 0; x < 8; ++x) From 646a07be40a7312f5682d837e913424c297afc2d Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 20:06:39 +0300 Subject: [PATCH 12/29] ui chances fixes --- src/injected/ui.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index b3cc1d300..7688f1c5f 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -8122,20 +8122,20 @@ void render_game_props() } if (submenu("Level generation flags")) { - ImGui::Text("Flags 1:"); auto flags = (int)g_state->level_gen->flags; auto flags2 = (int)g_state->level_gen->flags2; auto flags3 = (int)g_state->level_gen->flags3; + ImGui::SeparatorText("Flags 1"); for (int i = 0; i < 8; i++) { ImGui::CheckboxFlags(levelgen_flags[i], &flags, (int)std::pow(2, i)); } - ImGui::Text("Flags 2:"); + ImGui::SeparatorText("Flags 2"); for (int i = 0; i < 8; i++) { ImGui::CheckboxFlags(levelgen_flags2[i], &flags2, (int)std::pow(2, i)); } - ImGui::Text("Flags 3:"); + ImGui::SeparatorText("Flags 3"); for (int i = 0; i < 8; i++) { ImGui::CheckboxFlags(levelgen_flags3[i], &flags3, (int)std::pow(2, i)); @@ -8143,19 +8143,23 @@ void render_game_props() g_state->level_gen->flags = (uint8_t)flags; g_state->level_gen->flags2 = (uint8_t)flags2; g_state->level_gen->flags3 = (uint8_t)flags3; - ImGui::Text("Theme flags:"); - ImGui::Checkbox("Allow beehives##ThemeBeeHive", &g_state->current_theme->allow_beehive); - ImGui::Checkbox("Allow leprechauns##ThemeLeprechaun", &g_state->current_theme->allow_leprechaun); - + if (g_state->current_theme) + { + ImGui::SeparatorText("Theme flags"); + ImGui::Checkbox("Allow beehives##ThemeBeeHive", &g_state->current_theme->allow_beehive); + ImGui::Checkbox("Allow leprechauns##ThemeLeprechaun", &g_state->current_theme->allow_leprechaun); + } endmenu(); } if (submenu("Procedural chances")) { + static auto hide_zero = true; + ImGui::Checkbox("Hide 0% chances", &hide_zero); static auto render_procedural_chance = [](uint32_t id, LevelChanceDef& def) { int inverse_chance = g_state->level_gen->get_procedural_spawn_chance(id); std::string name = std::string(g_state->level_gen->get_procedural_spawn_chance_name(id).value_or(fmt::format("{}", id))); - if (def.chances.empty()) + if (def.chances.empty() || (hide_zero && inverse_chance == 0)) return; float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0; std::string all = fmt::format("{}", def.chances[0]); @@ -8167,6 +8171,8 @@ void render_game_props() static auto render_chance = [](int inverse_chance, const char* name) { + if (hide_zero && inverse_chance == 0) + return; float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0; std::string str = fmt::format("{:.3f}%", chance); ImGui::LabelText(str.c_str(), name); From e70b5cb7e1c1e0f840a90dfc56c50d800cabd00e Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 20:54:09 +0300 Subject: [PATCH 13/29] thank you for the useless warning --- src/injected/ui.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 7688f1c5f..22a217c23 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -8166,7 +8166,10 @@ void render_game_props() for (auto i = 1; i < def.chances.size(); ++i) all += "," + fmt::format("{}", def.chances[i]); std::string str = fmt::format("{:.3f}% ({})", chance, all); - ImGui::LabelText(str.c_str(), name.c_str()); + ImGui::Text("%s", name.c_str()); + auto w = ImGui::GetItemRectSize().x; + ImGui::SameLine(std::max(0.5f * ImGui::GetContentRegionMax().x, w), 4.f); + ImGui::Text("%s", str.c_str()); }; static auto render_chance = [](int inverse_chance, const char* name) @@ -8175,7 +8178,10 @@ void render_game_props() return; float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0; std::string str = fmt::format("{:.3f}%", chance); - ImGui::LabelText(str.c_str(), name); + ImGui::Text("%s", name); + auto w = ImGui::GetItemRectSize().x; + ImGui::SameLine(std::max(0.5f * ImGui::GetContentRegionMax().x, w), 4.f); + ImGui::Text("%s", str.c_str()); }; ImGui::SeparatorText("Monster chances"); From 54a993a5fd12cc43b39c8466bf0c10f4e746eefb Mon Sep 17 00:00:00 2001 From: Dregu Date: Thu, 12 Oct 2023 22:06:00 +0300 Subject: [PATCH 14/29] run level gen callbacks for transitions --- src/game_api/level_api.cpp | 64 ++++++++++++++++++++++++++++++++++++++ src/game_api/search.cpp | 24 ++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index 8ad4a15d1..6c9fa06e5 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -847,6 +847,62 @@ void level_gen(LevelGenSystem* level_gen_sys, float param_2, size_t param_3) g_levels_to_load.clear(); } +using TransGenFun = void(ThemeInfo*); +TransGenFun* g_trans_gen_trampoline{nullptr}; +TransGenFun* g_trans_gen2_trampoline{nullptr}; +using TransGenFun3 = void(size_t, size_t, ThemeInfo*); +TransGenFun3* g_trans_gen3_trampoline{nullptr}; +using TransGenFun4 = void(size_t, size_t, size_t, size_t, size_t, size_t); +TransGenFun4* g_trans_gen4_trampoline{nullptr}; +// generic transition hook +void trans_gen(ThemeInfo* theme) +{ + push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); + OnScopeExit pop{[] + { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }}; + + if (pre_level_generation()) + return; + g_trans_gen_trampoline(theme); + post_level_generation(); +} +// cosmic transition hook +void trans_gen2(ThemeInfo* theme) +{ + push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); + OnScopeExit pop{[] + { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }}; + + if (pre_level_generation()) + return; + g_trans_gen2_trampoline(theme); + post_level_generation(); +} +// duat transition hook +void trans_gen3(size_t a, size_t b, ThemeInfo* theme) +{ + push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); + OnScopeExit pop{[] + { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }}; + + if (pre_level_generation()) + return; + g_trans_gen3_trampoline(a, b, theme); + post_level_generation(); +} +// olmecship transition hook +void trans_gen4(size_t a, size_t b, size_t c, size_t d, size_t e, size_t f) +{ + push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); + OnScopeExit pop{[] + { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }}; + + if (pre_level_generation()) + return; + g_trans_gen4_trampoline(a, b, c, d, e, f); + post_level_generation(); +} + using LoadScreenFun = void(StateMemory*, size_t, size_t); LoadScreenFun* g_load_screen_trampoline{nullptr}; void load_screen(StateMemory* state, size_t param_2, size_t param_3) @@ -1500,6 +1556,10 @@ void LevelGenData::init() g_load_screen_trampoline = (LoadScreenFun*)get_address("load_screen_func"sv); g_unload_layer_trampoline = (UnloadLayerFun*)get_address("unload_layer"sv); g_init_layer_trampoline = (InitLayerFun*)get_address("init_layer"sv); + g_trans_gen_trampoline = (TransGenFun*)get_address("spawn_transition"sv); + g_trans_gen2_trampoline = (TransGenFun*)get_address("spawn_transition_cosmic"sv); + g_trans_gen3_trampoline = (TransGenFun3*)get_address("spawn_transition_duat"sv); + g_trans_gen4_trampoline = (TransGenFun4*)get_address("spawn_transition_olmecship"sv); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); @@ -1517,6 +1577,10 @@ void LevelGenData::init() DetourAttach((void**)&g_load_screen_trampoline, load_screen); DetourAttach((void**)&g_unload_layer_trampoline, unload_layer); DetourAttach((void**)&g_init_layer_trampoline, load_layer); + DetourAttach((void**)&g_trans_gen_trampoline, trans_gen); + DetourAttach((void**)&g_trans_gen2_trampoline, trans_gen2); + DetourAttach((void**)&g_trans_gen3_trampoline, trans_gen3); + DetourAttach((void**)&g_trans_gen4_trampoline, trans_gen4); const LONG error = DetourTransactionCommit(); if (error != NO_ERROR) diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 6e0acbc12..3ceaaccc0 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2054,6 +2054,30 @@ std::unordered_map g_address_rules{ PatternCommandBuffer{} .from_exe_base(0x22c061d0), }, + { + "spawn_transition"sv, + // TODO + PatternCommandBuffer{} + .from_exe_base(0x22afe5c0), + }, + { + "spawn_transition_cosmic"sv, + // TODO + PatternCommandBuffer{} + .from_exe_base(0x22b373b0), + }, + { + "spawn_transition_duat"sv, + // TODO + PatternCommandBuffer{} + .from_exe_base(0x22b34940), + }, + { + "spawn_transition_olmecship"sv, + // TODO + PatternCommandBuffer{} + .from_exe_base(0x22b2d350), + }, }; std::unordered_map g_cached_addresses; From a312d78bf17aea0a32f7522ce48a50276501bee5 Mon Sep 17 00:00:00 2001 From: Dregu Date: Fri, 13 Oct 2023 05:25:39 +0300 Subject: [PATCH 15/29] testing cutscene_behavior --- src/game_api/movable.hpp | 4 +- src/game_api/script/usertypes/entity_lua.cpp | 71 +++++++++++++++++--- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/game_api/movable.hpp b/src/game_api/movable.hpp index 06b8d257a..be63d573b 100644 --- a/src/game_api/movable.hpp +++ b/src/game_api/movable.hpp @@ -14,7 +14,7 @@ class CutsceneBehavior { public: virtual ~CutsceneBehavior(){}; - virtual void update() = 0; + virtual void update(Movable* e) = 0; // no more virtuals, it's possible that different sub classes have some extra variables as well }; @@ -24,7 +24,7 @@ class Movable : public Entity custom_map behaviors_map; custom_set behaviors; MovableBehavior* current_behavior; - CutsceneBehavior* ic8; + CutsceneBehavior* cutscene_behavior; union { /// {movex, movey} diff --git a/src/game_api/script/usertypes/entity_lua.cpp b/src/game_api/script/usertypes/entity_lua.cpp index daa7a3d4f..6b354cd36 100644 --- a/src/game_api/script/usertypes/entity_lua.cpp +++ b/src/game_api/script/usertypes/entity_lua.cpp @@ -12,15 +12,47 @@ #include // for min, max, swap, pair #include // for _Vector_iterator, vector, _Vector_... -#include "color.hpp" // for Color, Color::a, Color::b, Color::g -#include "custom_types.hpp" // for get_custom_types_map -#include "entities_chars.hpp" // for Player -#include "entity.hpp" // for Entity, EntityDB, Animation, Rect -#include "items.hpp" // for Inventory -#include "math.hpp" // for Quad, AABB -#include "movable.hpp" // for Movable, Movable::falling_timer -#include "render_api.hpp" // for RenderInfo, RenderInfo::flip_horiz... -#include "script/lua_backend.hpp" // for LuaBackend +#include "color.hpp" // for Color, Color::a, Color::b, Color::g +#include "containers/game_allocator.hpp" // for game_allocator +#include "custom_types.hpp" // for get_custom_types_map +#include "entities_chars.hpp" // for Player +#include "entity.hpp" // for Entity, EntityDB, Animation, Rect +#include "items.hpp" // for Inventory +#include "math.hpp" // for Quad, AABB +#include "movable.hpp" // for Movable, Movable::falling_timer +#include "render_api.hpp" // for RenderInfo, RenderInfo::flip_horiz... +#include "script/lua_backend.hpp" // for LuaBackend +#include "script/safe_cb.hpp" // for make_safe_cb + +class CustomCutsceneBehavior : public CutsceneBehavior +{ + public: + using CutsceneCb = bool(Movable*, uint32_t); + CustomCutsceneBehavior(sol::function cb); + ~CustomCutsceneBehavior(); + uint32_t frame{0}; + std::function func; + void update(Movable* movable); +}; + +CustomCutsceneBehavior::CustomCutsceneBehavior(sol::function cb) +{ + func = make_safe_cb(std::move(cb)); +} + +CustomCutsceneBehavior::~CustomCutsceneBehavior() +{ +} + +void CustomCutsceneBehavior::update(Movable* movable) +{ + if (func(movable, ++frame)) + { + game_allocator a; + a.destroy(movable->cutscene_behavior); + movable->cutscene_behavior = nullptr; + } +} namespace NEntity { @@ -347,6 +379,27 @@ void register_usertypes(sol::state& lua) movable_type["set_gravity"] = &Movable::set_gravity; movable_type["reset_gravity"] = &Movable::reset_gravity; movable_type["set_position"] = &Movable::set_position; + movable_type["process_input"] = &Movable::process_input; + movable_type["cutscene"] = sol::readonly(&Movable::cutscene_behavior); + movable_type["set_cutscene"] = [](Movable& movable, sol::optional cb) + { + if (movable.cutscene_behavior) + { + game_allocator a; + a.destroy(movable.cutscene_behavior); + movable.cutscene_behavior = nullptr; + } + if (cb.has_value()) + { + game_allocator a; + CustomCutsceneBehavior* p = a.allocate(1); + a.construct(p, cb.value()); + movable.cutscene_behavior = p; + } + }; + + lua.new_usertype("CutsceneBehavior", sol::no_constructor); + lua.new_usertype("MovableCutscene", sol::no_constructor, sol::base_classes, sol::bases()); lua["Entity"]["as_entity"] = &Entity::as; lua["Entity"]["as_movable"] = &Entity::as; From f3cdf00d43df9f640d80c86e03e514866eb15172 Mon Sep 17 00:00:00 2001 From: Dregu Date: Fri, 13 Oct 2023 17:06:32 +0300 Subject: [PATCH 16/29] fix spawn_player position --- src/game_api/spawn_api.cpp | 23 +++++++++++++++++++---- src/game_api/spawn_api.hpp | 2 +- src/injected/ui.cpp | 4 ++-- src/injected/ui_util.cpp | 2 +- src/injected/ui_util.hpp | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index 428f1d554..ab9244333 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -662,18 +662,33 @@ void init_spawn_hooks() } } -void spawn_player(int8_t player_slot, float x, float y) +int32_t spawn_player(int8_t player_slot, std::optional x, std::optional y) { if (player_slot < 1 || player_slot > 4) - return; + return -1; + auto state = State::get().ptr(); + auto& slot = state->items->player_select_slots[player_slot - 1]; + if (slot.character < to_id("ENT_TYPE_CHAR_ANA_SPELUNKY") || slot.character > to_id("ENT_TYPE_CHAR_CLASSIC_GUY")) + return -1; + if (state->items->player_count < player_slot) + state->items->player_count = player_slot; + slot.activated = true; push_spawn_type_flags(SPAWN_TYPE_SCRIPT); OnScopeExit pop{[] { pop_spawn_type_flags(SPAWN_TYPE_SCRIPT); }}; - using spawn_player_fun = void(Items*, uint8_t ps, float pos_x, float pos_y); + auto old_x = state->level_gen->spawn_x; + auto old_y = state->level_gen->spawn_y; + state->level_gen->spawn_x = x.value_or(old_x); + state->level_gen->spawn_y = y.value_or(old_y); + auto uid = (int32_t)state->next_entity_uid; + using spawn_player_fun = void(Items*, uint8_t ps); static auto spawn_player = (spawn_player_fun*)get_address("spawn_player"); - spawn_player(get_state_ptr()->items, player_slot - 1, x, y); + spawn_player(get_state_ptr()->items, player_slot - 1); + state->level_gen->spawn_x = old_x; + state->level_gen->spawn_y = old_y; + return uid; } int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer) diff --git a/src/game_api/spawn_api.hpp b/src/game_api/spawn_api.hpp index 2fed2ba2c..75745cb71 100644 --- a/src/game_api/spawn_api.hpp +++ b/src/game_api/spawn_api.hpp @@ -49,7 +49,7 @@ void pop_spawn_type_flags(SPAWN_TYPE flags); void init_spawn_hooks(); -void spawn_player(int8_t player_slot, float x, float y); +int32_t spawn_player(int8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt); int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer); int32_t spawn_shopkeeper(float x, float y, LAYER layer, ROOM_TEMPLATE room_template = 65); int32_t spawn_roomowner(ENT_TYPE owner_type, float x, float y, LAYER layer, int16_t room_template = -1); diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 22a217c23..1275fdc98 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -2324,7 +2324,7 @@ void respawn() if (!found) { g_state->items->player_inventories[i].health = 4; - UI::spawn_player(i, g_state->level_gen->spawn_x, g_state->level_gen->spawn_y); + UI::spawn_player(i); } } } @@ -8071,7 +8071,7 @@ void render_game_props() { g_state->items->player_inventories[i].health = 4; auto uid = g_state->next_entity_uid; - UI::spawn_player(i, spawn_x, spawn_y); + UI::spawn_player(i); auto player = get_entity_ptr(uid)->as(); player->set_position(spawn_x, spawn_y); } diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index d7c80041e..826d6ad58 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -754,7 +754,7 @@ int32_t UI::spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer, return uid; } -void UI::spawn_player(uint8_t player_slot, float x, float y) +void UI::spawn_player(uint8_t player_slot, std::optional x, std::optional y) { ::spawn_player(player_slot + 1, x, y); } diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp index d2325d4cd..52b05ca82 100644 --- a/src/injected/ui_util.hpp +++ b/src/injected/ui_util.hpp @@ -85,7 +85,7 @@ class UI static float get_spark_distance(SparkTrap* ent); static void save_progress(); static int32_t spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer, float vx, float vy); - static void spawn_player(uint8_t player_slot, float x, float y); + static void spawn_player(uint8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt); static std::pair spawn_position(); static void load_death_screen(); }; From a9c1f3e6ab8925841815bbf12f17f3ddc9e739aa Mon Sep 17 00:00:00 2001 From: Dregu Date: Fri, 13 Oct 2023 17:37:48 +0300 Subject: [PATCH 17/29] add layer hack to spawn_player --- src/game_api/spawn_api.cpp | 7 ++++++- src/game_api/spawn_api.hpp | 2 +- src/injected/ui.cpp | 2 +- src/injected/ui_util.cpp | 4 ++-- src/injected/ui_util.hpp | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index ab9244333..21d270a75 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -662,7 +662,7 @@ void init_spawn_hooks() } } -int32_t spawn_player(int8_t player_slot, std::optional x, std::optional y) +int32_t spawn_player(int8_t player_slot, std::optional x, std::optional y, std::optional layer) { if (player_slot < 1 || player_slot > 4) return -1; @@ -685,7 +685,12 @@ int32_t spawn_player(int8_t player_slot, std::optional x, std::optionalnext_entity_uid; using spawn_player_fun = void(Items*, uint8_t ps); static auto spawn_player = (spawn_player_fun*)get_address("spawn_player"); + // lazy? maybe. it's where it gets the state+0x1300 layer0 pointer + auto layer_off = get_address("spawn_player") + 0x3ea; + if (layer.has_value() && layer.value() == LAYER::BACK) + write_mem_recoverable("spawn_player_layer", layer_off, 0x1308, true); spawn_player(get_state_ptr()->items, player_slot - 1); + recover_mem("spawn_player_layer"); state->level_gen->spawn_x = old_x; state->level_gen->spawn_y = old_y; return uid; diff --git a/src/game_api/spawn_api.hpp b/src/game_api/spawn_api.hpp index 75745cb71..83ee3626f 100644 --- a/src/game_api/spawn_api.hpp +++ b/src/game_api/spawn_api.hpp @@ -49,7 +49,7 @@ void pop_spawn_type_flags(SPAWN_TYPE flags); void init_spawn_hooks(); -int32_t spawn_player(int8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt); +int32_t spawn_player(int8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt, std::optional layer = std::nullopt); int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer); int32_t spawn_shopkeeper(float x, float y, LAYER layer, ROOM_TEMPLATE room_template = 65); int32_t spawn_roomowner(ENT_TYPE owner_type, float x, float y, LAYER layer, int16_t room_template = -1); diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 1275fdc98..f5cf8ef56 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -2305,7 +2305,7 @@ void respawn() } return; } - for (int8_t i = 0; i < 4; ++i) + for (int8_t i = 0; i < g_state->items->player_count; ++i) { auto found = false; for (auto p : UI::get_players()) diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index 826d6ad58..69877d960 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -754,9 +754,9 @@ int32_t UI::spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer, return uid; } -void UI::spawn_player(uint8_t player_slot, std::optional x, std::optional y) +void UI::spawn_player(uint8_t player_slot, std::optional x, std::optional y, std::optional layer) { - ::spawn_player(player_slot + 1, x, y); + ::spawn_player(player_slot + 1, x, y, layer); } std::pair UI::spawn_position() diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp index 52b05ca82..224cee8f2 100644 --- a/src/injected/ui_util.hpp +++ b/src/injected/ui_util.hpp @@ -85,7 +85,7 @@ class UI static float get_spark_distance(SparkTrap* ent); static void save_progress(); static int32_t spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer, float vx, float vy); - static void spawn_player(uint8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt); + static void spawn_player(uint8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt, std::optional layer = std::nullopt); static std::pair spawn_position(); static void load_death_screen(); }; From 20eeb432901f19cee3eae93d6e153098d126379c Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 12:04:34 +0300 Subject: [PATCH 18/29] uglier hack that works better --- src/game_api/spawn_api.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index 21d270a75..1d59f3666 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -685,10 +685,13 @@ int32_t spawn_player(int8_t player_slot, std::optional x, std::optionalnext_entity_uid; using spawn_player_fun = void(Items*, uint8_t ps); static auto spawn_player = (spawn_player_fun*)get_address("spawn_player"); - // lazy? maybe. it's where it gets the state+0x1300 layer0 pointer - auto layer_off = get_address("spawn_player") + 0x3ea; + auto layer_off = (size_t)State::get().ptr() + 0x1300; + // move the back layer to front layer offset if spawning in back layer if (layer.has_value() && layer.value() == LAYER::BACK) - write_mem_recoverable("spawn_player_layer", layer_off, 0x1308, true); + { + auto layer1_ptr = memory_read(layer_off + 8); + write_mem_recoverable("spawn_player_layer", layer_off, layer1_ptr, true); + } spawn_player(get_state_ptr()->items, player_slot - 1); recover_mem("spawn_player_layer"); state->level_gen->spawn_x = old_x; From 07f696855d3b2a854541b16197e0fa762cffcaf0 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 12:19:13 +0300 Subject: [PATCH 19/29] make it not stupid --- src/game_api/spawn_api.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index 1d59f3666..14a06ec71 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -685,15 +685,12 @@ int32_t spawn_player(int8_t player_slot, std::optional x, std::optionalnext_entity_uid; using spawn_player_fun = void(Items*, uint8_t ps); static auto spawn_player = (spawn_player_fun*)get_address("spawn_player"); - auto layer_off = (size_t)State::get().ptr() + 0x1300; // move the back layer to front layer offset if spawning in back layer if (layer.has_value() && layer.value() == LAYER::BACK) - { - auto layer1_ptr = memory_read(layer_off + 8); - write_mem_recoverable("spawn_player_layer", layer_off, layer1_ptr, true); - } + std::swap(State::get().ptr()->layers[0], State::get().ptr()->layers[1]); spawn_player(get_state_ptr()->items, player_slot - 1); - recover_mem("spawn_player_layer"); + if (layer.has_value() && layer.value() == LAYER::BACK) + std::swap(State::get().ptr()->layers[0], State::get().ptr()->layers[1]); state->level_gen->spawn_x = old_x; state->level_gen->spawn_y = old_y; return uid; From a4495684b7477e55b439f1ce7a7cf27338ebe0f2 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 12:33:09 +0300 Subject: [PATCH 20/29] remove defaults --- src/game_api/spawn_api.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_api/spawn_api.hpp b/src/game_api/spawn_api.hpp index 83ee3626f..b618399b0 100644 --- a/src/game_api/spawn_api.hpp +++ b/src/game_api/spawn_api.hpp @@ -49,7 +49,7 @@ void pop_spawn_type_flags(SPAWN_TYPE flags); void init_spawn_hooks(); -int32_t spawn_player(int8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt, std::optional layer = std::nullopt); +int32_t spawn_player(int8_t player_slot, std::optional x, std::optional y, std::optional layer); int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer); int32_t spawn_shopkeeper(float x, float y, LAYER layer, ROOM_TEMPLATE room_template = 65); int32_t spawn_roomowner(ENT_TYPE owner_type, float x, float y, LAYER layer, int16_t room_template = -1); From 5425ab76ee19e9fdf2806b381beb06283fec3fff Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 12:40:56 +0300 Subject: [PATCH 21/29] fix docs --- docs/game_data/spel2.lua | 34 +++++++++++++++++--- docs/parse_source.py | 4 ++- docs/src/includes/_enums.md | 10 +++++- docs/src/includes/_events.md | 58 ++++++++++++++++++++++++++++++++++- docs/src/includes/_globals.md | 47 +++++++++++++++++++++++++++- docs/src/includes/_types.md | 17 ++++++++++ 6 files changed, 162 insertions(+), 8 deletions(-) diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index 2b6b467d8..bb5ea8eae 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -383,10 +383,11 @@ function spawn_unrolled_player_rope(x, y, layer, texture, max_length) end ---Spawn a player in given location, if player of that slot already exist it will spawn clone, the game may crash as this is very unexpected situation ---If you want to respawn a player that is a ghost, set in his Inventory `health` to above 0, and `time_of_death` to 0 and call this function, the ghost entity will be removed automatically ---@param player_slot integer ----@param x number ----@param y number ----@return nil -function spawn_player(player_slot, x, y) end +---@param x number? +---@param y number? +---@param layer LAYER? +---@return integer +function spawn_player(player_slot, x, y, layer) end ---Spawn the PlayerGhost entity, it will not move and not be connected to any player, you can then use [steal_input](https://spelunky-fyi.github.io/overlunky/#steal_input) and send_input to controll it ---or change it's `player_inputs` to the `input` of real player so he can control it directly ---@param char_type ENT_TYPE @@ -1233,6 +1234,24 @@ function set_frametime_unfocused(frametime) end ---Get engine target frametime when game is unfocused (1/framerate, default 1/33). ---@return double? function get_frametime_unfocused() end +---Destroys all layers and all entities in the level. Usually a bad idea, unless you also call create_level and spawn the player back in. +---@return nil +function destroy_level() end +---Destroys a layer and all entities in it. +---@param layer integer +---@return nil +function destroy_layer(layer) end +---Initializes an empty front and back layer that don't currently exist. Does nothing(?) if layers already exist. +---@return nil +function create_level() end +---Initializes an empty layer that doesn't currently exist. +---@param layer integer +---@return nil +function create_layer(layer) end +---Setting to false disables the death screen from popping up for any usual reason, can still load manually +---@param enable boolean +---@return nil +function set_death_enabled(enable) end ---@return boolean function toast_visible() end ---@return boolean @@ -2401,6 +2420,9 @@ function Entity:overlaps_with(other) end ---@field set_gravity fun(self, gravity: number): nil @Force the gravity for this entity. Will override anything set by special states like swimming too, unless you reset it. Default 1.0 ---@field reset_gravity fun(self): nil @Remove the gravity hook and reset to defaults ---@field set_position fun(self, to_x: number, to_y: number): nil @Set the absolute position of an entity and offset all rendering related things accordingly to teleport without any interpolation or graphical glitches. If the camera is focused on the entity, it is also moved. + ---@field process_input fun(self): nil + ---@field cutscene CutsceneBehavior + ---@field set_cutscene any @[](Movable&movable ---@field get_base_behavior fun(self, state_id: integer): VanillaMovableBehavior @Gets a vanilla behavior from this movable, needs to be called before `clear_behaviors`
but the returned values are still valid after a call to `clear_behaviors` ---@field add_behavior fun(self, behavior: MovableBehavior): nil @Add a behavior to this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior` ---@field clear_behavior fun(self, behavior: MovableBehavior): nil @Clear a specific behavior of this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`, a behavior with this behaviors `state_id` may be required to
run this movables statemachine without crashing, so add a new one if you are not sure @@ -2479,6 +2501,10 @@ function Movable:generic_update_world(disable_gravity) end ---@return nil function Movable:generic_update_world(move, sprint_factor, disable_gravity, on_rope) end +---@class CutsceneBehavior + +---@class MovableCutscene : CutsceneBehavior + ---@class PowerupCapable : Movable ---@field remove_powerup fun(self, powerup_type: ENT_TYPE): nil @Removes a currently applied powerup. Specify `ENT_TYPE.ITEM_POWERUP_xxx`, not `ENT_TYPE.ITEM_PICKUP_xxx`! Removing the Eggplant crown does not seem to undo the throwing of eggplants, the other powerups seem to work. ---@field give_powerup fun(self, powerup_type: ENT_TYPE): nil @Gives the player/monster the specified powerup. Specify `ENT_TYPE.ITEM_POWERUP_xxx`, not `ENT_TYPE.ITEM_PICKUP_xxx`! Giving true crown to a monster crashes the game. diff --git a/docs/parse_source.py b/docs/parse_source.py index dbd0cec7c..4eadd652b 100644 --- a/docs/parse_source.py +++ b/docs/parse_source.py @@ -125,6 +125,8 @@ cpp_type_exceptions = [ "Players", + "CutsceneBehavior", + "CustomCutsceneBehavior", ] not_functions = [ "players", @@ -892,7 +894,7 @@ def run_parse(): if not var: continue var = var.split(",") - if(len(var) > 1): + if len(var) > 1: vars.append({"name": var[0], "type": var[1]}) enums.append({"name": name, "vars": vars}) data = open(file, "r").read() diff --git a/docs/src/includes/_enums.md b/docs/src/includes/_enums.md index f49185efd..3ee27a974 100644 --- a/docs/src/includes/_enums.md +++ b/docs/src/includes/_enums.md @@ -783,7 +783,7 @@ Name | Data | Description [SAVE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.SAVE) | ON::SAVE | Params: [SaveContext](#SaveContext) save_ctx
Runs at the same times as [ON](#ON).[SCREEN](#SCREEN), but receives the save_ctx
[LOAD](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.LOAD) | ON::LOAD | Params: [LoadContext](#LoadContext) load_ctx
Runs as soon as your script is loaded, including reloads, then never again
[PRE_LOAD_LEVEL_FILES](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LOAD_LEVEL_FILES) | ON::PRE_LOAD_LEVEL_FILES | Params: [PreLoadLevelFilesContext](#PreLoadLevelFilesContext) load_level_ctx
Runs right before level files would be loaded
-[PRE_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_GENERATION) | ON::PRE_LEVEL_GENERATION | Runs before any level generation, no entities should exist at this point
+[PRE_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_GENERATION) | ON::PRE_LEVEL_GENERATION | Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens. Return true to stop normal level generation.
[PRE_LOAD_SCREEN](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LOAD_SCREEN) | ON::PRE_LOAD_SCREEN | Runs right before loading a new screen based on screen_next. Return true from callback to block the screen from loading.
[POST_ROOM_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_ROOM_GENERATION) | ON::POST_ROOM_GENERATION | Params: [PostRoomGenerationContext](#PostRoomGenerationContext) room_gen_ctx
Runs right after all rooms are generated before entities are spawned
[POST_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_GENERATION) | ON::POST_LEVEL_GENERATION | Runs right after level generation is done, before any entities are updated
@@ -818,6 +818,14 @@ Name | Data | Description [PRE_UPDATE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_UPDATE) | ON::PRE_UPDATE | Runs before the State is updated, runs always (menu, settings, camp, game, arena, online etc.) with the game engine, typically 60FPS
Return behavior: return true to stop futher PRE_UPDATE callbacks from executing and don't update the state (this will essentially freeze the game engine)
[POST_UPDATE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_UPDATE) | ON::POST_UPDATE | Runs right after the State is updated, runs always (menu, settings, camp, game, arena, online etc.) with the game engine, typically 60FPS
[USER_DATA](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.USER_DATA) | ON::USER_DATA | Params: [Entity](#Entity) ent
Runs on all changes to [Entity](#Entity).user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels.
+[PRE_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_CREATION) | ON::PRE_LEVEL_CREATION | Runs right before the front layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+[POST_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_CREATION) | ON::POST_LEVEL_CREATION | Runs right after the back layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+[PRE_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_CREATION) | ON::PRE_LAYER_CREATION | Params: [LAYER](#LAYER) layer
Runs right before a layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+[POST_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_CREATION) | ON::POST_LAYER_CREATION | Params: [LAYER](#LAYER) layer
Runs right after a layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+[PRE_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_DESTRUCTION) | ON::PRE_LEVEL_DESTRUCTION | Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+[POST_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_DESTRUCTION) | ON::POST_LEVEL_DESTRUCTION | Runs right after the current level has been unloaded and all entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+[PRE_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_DESTRUCTION) | ON::PRE_LAYER_DESTRUCTION | Params: [LAYER](#LAYER) layer
Runs right before a layer is unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+[POST_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_DESTRUCTION) | ON::POST_LAYER_DESTRUCTION | Params: [LAYER](#LAYER) layer
Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
## PARTICLEEMITTER diff --git a/docs/src/includes/_events.md b/docs/src/includes/_events.md index 1e9d26acf..309a790b1 100644 --- a/docs/src/includes/_events.md +++ b/docs/src/includes/_events.md @@ -290,7 +290,7 @@ Params: [PreLoadLevelFilesContext](#PreLoadLevelFilesContext) load_level_ctx
Search script examples for [ON.PRE_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_GENERATION) -Runs before any level generation, no entities should exist at this point
+Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens. Return true to stop normal level generation.
## ON.PRE_LOAD_SCREEN @@ -549,3 +549,59 @@ Runs right after the State is updated, runs always (menu, settings, camp, game, > Search script examples for [ON.USER_DATA](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.USER_DATA) Params: [Entity](#Entity) ent
Runs on all changes to [Entity](#Entity).user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels.
+ +## ON.PRE_LEVEL_CREATION + + +> Search script examples for [ON.PRE_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_CREATION) + +Runs right before the front layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+ +## ON.POST_LEVEL_CREATION + + +> Search script examples for [ON.POST_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_CREATION) + +Runs right after the back layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+ +## ON.PRE_LAYER_CREATION + + +> Search script examples for [ON.PRE_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_CREATION) + +Params: [LAYER](#LAYER) layer
Runs right before a layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+ +## ON.POST_LAYER_CREATION + + +> Search script examples for [ON.POST_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_CREATION) + +Params: [LAYER](#LAYER) layer
Runs right after a layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+ +## ON.PRE_LEVEL_DESTRUCTION + + +> Search script examples for [ON.PRE_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_DESTRUCTION) + +Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+ +## ON.POST_LEVEL_DESTRUCTION + + +> Search script examples for [ON.POST_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_DESTRUCTION) + +Runs right after the current level has been unloaded and all entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+ +## ON.PRE_LAYER_DESTRUCTION + + +> Search script examples for [ON.PRE_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_DESTRUCTION) + +Params: [LAYER](#LAYER) layer
Runs right before a layer is unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+ +## ON.POST_LAYER_DESTRUCTION + + +> Search script examples for [ON.POST_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_DESTRUCTION) + +Params: [LAYER](#LAYER) layer
Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index 782963d41..4c02ed048 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -1205,6 +1205,24 @@ Depending on the image size, this can take a moment, preferably don't create the Create image from file, cropped to the geometry provided. Returns a tuple containing id, width and height. Depending on the image size, this can take a moment, preferably don't create them dynamically, rather create all you need in global scope so it will load them as soon as the game starts +### create_layer + + +> Search script examples for [create_layer](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=create_layer) + +#### nil create_layer(int layer) + +Initializes an empty layer that doesn't currently exist. + +### create_level + + +> Search script examples for [create_level](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=create_level) + +#### nil create_level() + +Initializes an empty front and back layer that don't currently exist. Does nothing(?) if layers already exist. + ### destroy_grid @@ -1217,6 +1235,24 @@ Depending on the image size, this can take a moment, preferably don't create the Destroy the grid entity (by uid or position), and its item entities, removing them from the grid without dropping particles or gold. Will also destroy monsters or items that are standing on a linked activefloor or chain, though excludes [MASK](#MASK).PLAYER to prevent crashes +### destroy_layer + + +> Search script examples for [destroy_layer](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=destroy_layer) + +#### nil destroy_layer(int layer) + +Destroys a layer and all entities in it. + +### destroy_level + + +> Search script examples for [destroy_level](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=destroy_level) + +#### nil destroy_level() + +Destroys all layers and all entities in the level. Usually a bad idea, unless you also call create_level and spawn the player back in. + ### disable_floor_embeds @@ -1593,6 +1629,15 @@ Set the current adventure seed pair Same as `Player.set_heart_color` +### set_death_enabled + + +> Search script examples for [set_death_enabled](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_death_enabled) + +#### nil set_death_enabled(bool enable) + +Setting to false disables the death screen from popping up for any usual reason, can still load manually + ### set_ending_unlock @@ -2995,7 +3040,7 @@ Short for [spawn_entity_over](#spawn_entity_over) > Search script examples for [spawn_player](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=spawn_player) -#### nil spawn_player(int player_slot, float x, float y) +#### int spawn_player(int player_slot, optional x, optional y, optional<[LAYER](#LAYER)> layer) Spawn a player in given location, if player of that slot already exist it will spawn clone, the game may crash as this is very unexpected situation If you want to respawn a player that is a ghost, set in his [Inventory](#Inventory) `health` to above 0, and `time_of_death` to 0 and call this function, the ghost entity will be removed automatically diff --git a/docs/src/includes/_types.md b/docs/src/includes/_types.md index 6637f38fc..d66ea5c59 100644 --- a/docs/src/includes/_types.md +++ b/docs/src/includes/_types.md @@ -640,6 +640,12 @@ tuple<int, int, int, int> | [get_rgba()](https://github.com/spelunky-fyi/o [uColor](#Aliases) | [get_ucolor()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_ucolor) | Returns the `uColor` used in `GuiDrawContext` drawing functions [Color](#Color)& | [set_ucolor(const uColor color)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_ucolor) | Changes color based on given [uColor](#Aliases) +### CutsceneBehavior + + +Type | Name | Description +---- | ---- | ----------- + ### Hud @@ -757,6 +763,14 @@ Type | Name | Description int | [get_state_id()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_state_id) | int | [get_state_id()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_state_id) | Get the `state_id` of a behavior, this is the id that needs to be returned from a behavior's
`get_next_state_id` to enter this state, given that the behavior is added to the movable. +### MovableCutscene + +Derived from [CutsceneBehavior](#CutsceneBehavior) + + +Type | Name | Description +---- | ---- | ----------- + ### PRNG [PRNG](#PRNG) (short for Pseudo-Random-Number-Generator) holds 10 128bit wide buffers of memory that are mutated on every generation of a random number. @@ -6648,6 +6662,9 @@ int | [get_behavior()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q= nil | [set_gravity(float gravity)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_gravity) | Force the gravity for this entity. Will override anything set by special states like swimming too, unless you reset it. Default 1.0 nil | [reset_gravity()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=reset_gravity) | Remove the gravity hook and reset to defaults nil | [set_position(float to_x, float to_y)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_position) | Set the absolute position of an entity and offset all rendering related things accordingly to teleport without any interpolation or graphical glitches. If the camera is focused on the entity, it is also moved. +nil | [process_input()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=process_input) | +[CutsceneBehavior](#CutsceneBehavior) | [cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=cutscene) | + | [set_cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_cutscene) | [VanillaMovableBehavior](#VanillaMovableBehavior) | [get_base_behavior(int state_id)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_base_behavior) | Gets a vanilla behavior from this movable, needs to be called before `clear_behaviors`
but the returned values are still valid after a call to `clear_behaviors` nil | [add_behavior(MovableBehavior behavior)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=add_behavior) | Add a behavior to this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior` nil | [clear_behavior(MovableBehavior behavior)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=clear_behavior) | Clear a specific behavior of this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`, a behavior with this behaviors `state_id` may be required to
run this movables statemachine without crashing, so add a new one if you are not sure From 3b62a272648836edac78f7cd40adaef8047561eb Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 13:40:45 +0300 Subject: [PATCH 22/29] add patterns for new patterns --- src/game_api/search.cpp | 42 +++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 3ceaaccc0..3919135a2 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2044,39 +2044,57 @@ std::unordered_map g_address_rules{ }, { "init_layer"sv, - // TODO + // called a lot in load_screen, for both layers in every screen that has layers PatternCommandBuffer{} - .from_exe_base(0x228b58f0), + .find_inst("48 8d 7e 40 c7 44 24 2c 00 01 00 00"_gh) + .at_exe() + .function_start(), + //.from_exe_base(0x228b58f0), }, { "dead_players"sv, - // TODO + // I guess it writes 14 to screen_next before the death screen pops up PatternCommandBuffer{} - .from_exe_base(0x22c061d0), + .find_inst("4c 8b b8 e8 12 00 00 48 8b 80 f0 12 00 00"_gh) + .at_exe() + .function_start(), + //.from_exe_base(0x22c061d0), }, { "spawn_transition"sv, - // TODO + // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose PatternCommandBuffer{} - .from_exe_base(0x22afe5c0), + .find_inst("0f b6 45 75 48 8b 5c c2 08"_gh) + .at_exe() + .function_start(), + //.from_exe_base(0x22afe5c0), }, { "spawn_transition_cosmic"sv, - // TODO + // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose PatternCommandBuffer{} - .from_exe_base(0x22b373b0), + .find_inst("48 b9 00 00 40 40 00 00 e0 42 48 89 8a 18 01 00 00"_gh) + .at_exe() + .function_start(), + //.from_exe_base(0x22b373b0), }, { "spawn_transition_duat"sv, - // TODO + // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose PatternCommandBuffer{} - .from_exe_base(0x22b34940), + .find_inst("48 8b 85 f8 12 00 00 0f b6 4d 75 48 8b 4c c8 08 48 8b 01 ff 90 d0 00 00 00"_gh) + .at_exe() + .function_start(), + //.from_exe_base(0x22b34940), }, { "spawn_transition_olmecship"sv, - // TODO + // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose PatternCommandBuffer{} - .from_exe_base(0x22b2d350), + .find_inst("48 89 f1 ba 63 01 00 00"_gh) + .at_exe() + .function_start(), + //.from_exe_base(0x22b2d350), }, }; std::unordered_map g_cached_addresses; From 55584e9a61efdb128c44c9fb4f2633365139522a Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 14:44:05 +0300 Subject: [PATCH 23/29] fix clear_callback in movable virtual hooks --- src/game_api/script/usertypes/vtables_lua.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game_api/script/usertypes/vtables_lua.cpp b/src/game_api/script/usertypes/vtables_lua.cpp index 21fdb59c6..5cb5f67f7 100644 --- a/src/game_api/script/usertypes/vtables_lua.cpp +++ b/src/game_api/script/usertypes/vtables_lua.cpp @@ -114,6 +114,9 @@ void register_usertypes(sol::state& lua) { Entity* ent = get_entity_ptr(uid); entity_vtable.unhook(ent, callback_id); + movable_vtable.unhook(ent, callback_id); + floor_vtable.unhook(ent, callback_id); + door_vtable.unhook(ent, callback_id); }); HookHandler::set_hook_dtor_impl( @@ -150,6 +153,9 @@ void register_usertypes(sol::state& lua) [](Entity* ent, std::uint32_t callback_id) { entity_vtable.unhook(ent, callback_id); + movable_vtable.unhook(ent, callback_id); + floor_vtable.unhook(ent, callback_id); + door_vtable.unhook(ent, callback_id); }); } }; // namespace NVTables From 0aedff0af62c88ba562e75fbe84cd298b91be593 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 16:30:32 +0300 Subject: [PATCH 24/29] change some patterns to vtable offsets --- src/game_api/level_api.cpp | 15 ++++++++++++--- src/game_api/level_api.hpp | 5 +++++ src/game_api/script/lua_vm.cpp | 14 +++++++++----- src/game_api/search.cpp | 30 +++++++++++++----------------- src/game_api/virtual_table.hpp | 1 + 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index 6c9fa06e5..6f2af4b67 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -878,17 +878,20 @@ void trans_gen2(ThemeInfo* theme) g_trans_gen2_trampoline(theme); post_level_generation(); } -// duat transition hook +// cog-duat transition hook void trans_gen3(size_t a, size_t b, ThemeInfo* theme) { push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); OnScopeExit pop{[] { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }}; - if (pre_level_generation()) + auto state = State::get().ptr(); + // trampoline will call the generic trans_gen if not going to duat + if (state->theme_next == 12 && pre_level_generation()) return; g_trans_gen3_trampoline(a, b, theme); - post_level_generation(); + if (state->theme_next == 12) + post_level_generation(); } // olmecship transition hook void trans_gen4(size_t a, size_t b, size_t c, size_t d, size_t e, size_t f) @@ -1893,6 +1896,12 @@ void LevelGenSystem::populate_level_hook(ThemeInfo* self, uint64_t param_2, uint original(self, param_2, param_3, param_4); } +void LevelGenSystem::populate_transition_hook(ThemeInfo* self, PopulateTransitionFun* original) +{ + pre_level_generation(); + original(self); + post_level_generation(); +} void LevelGenSystem::do_procedural_spawn_hook(ThemeInfo* self, SpawnInfo* spawn_info, DoProceduralSpawnFun* original) { push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_PROCEDURAL); diff --git a/src/game_api/level_api.hpp b/src/game_api/level_api.hpp index 88b444911..65988f426 100644 --- a/src/game_api/level_api.hpp +++ b/src/game_api/level_api.hpp @@ -423,12 +423,17 @@ struct LevelGenSystem { hook_impl.template hook(theme, &populate_level_hook); hook_impl.template hook(theme, &do_procedural_spawn_hook); + // this didn't work right + // hook_impl.template hook(theme, &populate_transition_hook); } } using PopulateLevelFun = void(ThemeInfo*, uint64_t, uint64_t, uint64_t); static void populate_level_hook(ThemeInfo*, uint64_t, uint64_t, uint64_t, PopulateLevelFun*); + using PopulateTransitionFun = void(ThemeInfo*); + static void populate_transition_hook(ThemeInfo*, PopulateTransitionFun*); + using DoProceduralSpawnFun = void(ThemeInfo*, SpawnInfo*); static void do_procedural_spawn_hook(ThemeInfo*, SpawnInfo*, DoProceduralSpawnFun*); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 523598a4a..6b325a136 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -95,6 +95,7 @@ #include "usertypes/texture_lua.hpp" // for register_usertypes #include "usertypes/vanilla_render_lua.hpp" // for VanillaRenderContext #include "usertypes/vtables_lua.hpp" // for register_usertypes +#include "virtual_table.hpp" struct Illumination; @@ -1892,13 +1893,16 @@ end /// Disable all crust item spawns, returns whether they were already disabled before the call lua["disable_floor_embeds"] = disable_floor_embeds; - /// Get the address for a pattern name - lua["get_address"] = get_address; + /// Get the rva for a pattern name, used for debugging. + lua["get_rva"] = [](std::string_view address_name) + { + return fmt::format("{:x}", get_address(address_name) - Memory::get().at_exe(0)); + }; - /// Get the rva for a pattern name - lua["get_rva"] = [](std::string_view address_name) -> size_t + /// Get the rva for a vtable offset and index, used for debugging. + lua["get_virtual_rva"] = [](VTABLE_OFFSET offset, uint32_t index) { - return get_address(address_name) - Memory::get().at_exe(0); + return fmt::format("{:x}", get_virtual_function_address(offset, index)); }; /// Log to spelunky.log diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 3919135a2..074c11b27 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2053,7 +2053,7 @@ std::unordered_map g_address_rules{ }, { "dead_players"sv, - // I guess it writes 14 to screen_next before the death screen pops up + // I guess it writes 14 to screen_next before the death screen pops up. Apparently it's a SCREEN_LEVEL virtual too. PatternCommandBuffer{} .find_inst("4c 8b b8 e8 12 00 00 48 8b 80 f0 12 00 00"_gh) .at_exe() @@ -2062,38 +2062,34 @@ std::unordered_map g_address_rules{ }, { "spawn_transition"sv, - // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose + // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION PatternCommandBuffer{} - .find_inst("0f b6 45 75 48 8b 5c c2 08"_gh) - .at_exe() - .function_start(), + .get_virtual_function_address(VTABLE_OFFSET::THEME_DWELLING, VIRT_FUNC::THEME_SPAWN_TRANSITION) + .at_exe(), //.from_exe_base(0x22afe5c0), }, { "spawn_transition_cosmic"sv, - // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose + // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION PatternCommandBuffer{} - .find_inst("48 b9 00 00 40 40 00 00 e0 42 48 89 8a 18 01 00 00"_gh) - .at_exe() - .function_start(), + .get_virtual_function_address(VTABLE_OFFSET::THEME_COSMICOCEAN, VIRT_FUNC::THEME_SPAWN_TRANSITION) + .at_exe(), //.from_exe_base(0x22b373b0), }, { "spawn_transition_duat"sv, - // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose + // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION PatternCommandBuffer{} - .find_inst("48 8b 85 f8 12 00 00 0f b6 4d 75 48 8b 4c c8 08 48 8b 01 ff 90 d0 00 00 00"_gh) - .at_exe() - .function_start(), + .get_virtual_function_address(VTABLE_OFFSET::THEME_CITY_OF_GOLD, VIRT_FUNC::THEME_SPAWN_TRANSITION) + .at_exe(), //.from_exe_base(0x22b34940), }, { "spawn_transition_olmecship"sv, - // I hooked ThemeInfo::spawn_transition to find these, but didn't actually want to hook the vtable for this purpose + // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION PatternCommandBuffer{} - .find_inst("48 89 f1 ba 63 01 00 00"_gh) - .at_exe() - .function_start(), + .get_virtual_function_address(VTABLE_OFFSET::THEME_BASECAMP, VIRT_FUNC::THEME_SPAWN_TRANSITION) + .at_exe(), //.from_exe_base(0x22b2d350), }, }; diff --git a/src/game_api/virtual_table.hpp b/src/game_api/virtual_table.hpp index 6d611fae1..8a4a3c728 100644 --- a/src/game_api/virtual_table.hpp +++ b/src/game_api/virtual_table.hpp @@ -25,6 +25,7 @@ enum class VIRT_FUNC ENTITY_COLLISION2 = 26, MOVABLE_DAMAGE = 48, LOGIC_PERFORM = 1, + THEME_SPAWN_TRANSITION = 21, }; enum class VTABLE_OFFSET From 27df88c359dd32f50d47c06022db6d36e2e2e561 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 16:37:10 +0300 Subject: [PATCH 25/29] fix spawn_player uid --- src/game_api/spawn_api.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index 14a06ec71..7cf829e97 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -682,7 +682,6 @@ int32_t spawn_player(int8_t player_slot, std::optional x, std::optionallevel_gen->spawn_y; state->level_gen->spawn_x = x.value_or(old_x); state->level_gen->spawn_y = y.value_or(old_y); - auto uid = (int32_t)state->next_entity_uid; using spawn_player_fun = void(Items*, uint8_t ps); static auto spawn_player = (spawn_player_fun*)get_address("spawn_player"); // move the back layer to front layer offset if spawning in back layer @@ -693,7 +692,10 @@ int32_t spawn_player(int8_t player_slot, std::optional x, std::optionallayers[0], State::get().ptr()->layers[1]); state->level_gen->spawn_x = old_x; state->level_gen->spawn_y = old_y; - return uid; + auto player = state->items->player(player_slot - 1); + if (player) + return player->uid; + return -1; } int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer) From 2fd3f4bbfc4d5b12d5e299d8e1e11818bc8200f2 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 16:51:56 +0300 Subject: [PATCH 26/29] update docs --- docs/game_data/spel2.lua | 13 ++++---- docs/generate.py | 6 ++-- docs/src/includes/_globals.md | 54 +++++++++++++++++----------------- src/game_api/script/lua_vm.cpp | 4 +-- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index bb5ea8eae..05edc1f2e 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -1135,14 +1135,15 @@ function update_liquid_collision_at(x, y, add) end ---@param disable boolean ---@return boolean function disable_floor_embeds(disable) end ----Get the address for a pattern name +---Get the rva for a pattern name, used for debugging. ---@param address_name string ----@return integer -function get_address(address_name) end ----Get the rva for a pattern name ----@param address_name string ----@return integer +---@return string function get_rva(address_name) end +---Get the rva for a vtable offset and index, used for debugging. +---@param offset VTABLE_OFFSET +---@param index integer +---@return string +function get_virtual_rva(offset, index) end ---Log to spelunky.log ---@param message string ---@return nil diff --git a/docs/generate.py b/docs/generate.py index 01bea7844..339d7945c 100644 --- a/docs/generate.py +++ b/docs/generate.py @@ -393,7 +393,7 @@ def print_lf(lf): cat = "Message functions" elif any( subs in func["name"] - for subs in ["get_address", "get_rva", "raise", "dump_network"] + for subs in ["get_rva", "get_virtual_rva", "raise", "dump_network"] ): cat = "Debug functions" elif any(subs in func["name"] for subs in ["_option"]): @@ -474,7 +474,7 @@ def print_lf(lf): cat = "Particle functions" elif any(subs in func["name"] for subs in ["string", "_name"]): cat = "String functions" - elif any(subs in func["name"] for subs in ["udp_"]): + elif any(subs in func["name"] for subs in ["udp_", "http_"]): cat = "Network functions" elif any(subs in func["name"] for subs in ["illuminati"]): cat = "Lighting functions" @@ -630,7 +630,7 @@ def print_lf(lf): "QuestsInfo", "PlayerSlot", "JournalProgressStickerSlot", - "JournalProgressStainSlot" + "JournalProgressStainSlot", ] ): cat = "State types" diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index 4c02ed048..eb275c7a0 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -351,23 +351,23 @@ If you set such a callback and then play the same sound yourself you have to wai Hook the sendto and recvfrom functions and start dumping network data to terminal -### get_address +### get_rva -> Search script examples for [get_address](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_address) +> Search script examples for [get_rva](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_rva) -#### size_t get_address(string_view address_name) +#### string get_rva(string_view address_name) -Get the address for a pattern name +Get the rva for a pattern name, used for debugging. -### get_rva +### get_virtual_rva -> Search script examples for [get_rva](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_rva) +> Search script examples for [get_virtual_rva](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_virtual_rva) -#### size_t get_rva(string_view address_name) +#### string get_virtual_rva(VTABLE_OFFSET offset, int index) -Get the rva for a pattern name +Get the rva for a vtable offset and index, used for debugging. ### raise @@ -1431,25 +1431,6 @@ Grow pole from `GROWABLE_CLIMBING_POLE` entities in a level, `area` default is w Grow vines from `GROWABLE_VINE` and `VINE_TREE_TOP` entities in a level, `area` default is whole level, `destroy_broken` default is false -### http_get - - -> Search script examples for [http_get](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get) - -#### optional<string> http_get(string url) - -Send a synchronous HTTP GET request and return response as a string or nil on an error - -### http_get_async - - -> Search script examples for [http_get_async](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get_async) - -#### HttpRequest http_get_async(string url, function on_data) - -Send an asynchronous HTTP GET request and run the callback when done. If there is an error, response will be nil and vice versa. -The callback signature is nil on_data(string response, string error) - ### import @@ -2111,6 +2092,25 @@ be returned instead. ## Network functions +### http_get + + +> Search script examples for [http_get](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get) + +#### optional<string> http_get(string url) + +Send a synchronous HTTP GET request and return response as a string or nil on an error + +### http_get_async + + +> Search script examples for [http_get_async](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get_async) + +#### HttpRequest http_get_async(string url, function on_data) + +Send an asynchronous HTTP GET request and run the callback when done. If there is an error, response will be nil and vice versa. +The callback signature is nil on_data(string response, string error) + ### udp_listen diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 6b325a136..2343cc2da 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -1894,13 +1894,13 @@ end lua["disable_floor_embeds"] = disable_floor_embeds; /// Get the rva for a pattern name, used for debugging. - lua["get_rva"] = [](std::string_view address_name) + lua["get_rva"] = [](std::string_view address_name) -> std::string { return fmt::format("{:x}", get_address(address_name) - Memory::get().at_exe(0)); }; /// Get the rva for a vtable offset and index, used for debugging. - lua["get_virtual_rva"] = [](VTABLE_OFFSET offset, uint32_t index) + lua["get_virtual_rva"] = [](VTABLE_OFFSET offset, uint32_t index) -> std::string { return fmt::format("{:x}", get_virtual_function_address(offset, index)); }; From d375795eb0ef0b784beafe5eedc459744db9aa15 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 17:46:37 +0300 Subject: [PATCH 27/29] remove cutscenes, add some input helpers --- docs/game_data/spel2.lua | 14 ++++-- docs/src/includes/_globals.md | 18 ++++++++ docs/src/includes/_types.md | 10 +--- src/game_api/script/lua_vm.cpp | 32 +++++++++++++ src/game_api/script/usertypes/entity_lua.cpp | 48 ++------------------ 5 files changed, 65 insertions(+), 57 deletions(-) diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index 05edc1f2e..e29918e01 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -1253,6 +1253,16 @@ function create_layer(layer) end ---@param enable boolean ---@return nil function set_death_enabled(enable) end +---Converts INPUTS to (x, y, BUTTON) +---@param inputs INPUTS +---@return number, number, BUTTON +function inputs_to_buttons(inputs) end +---Converts (x, y, BUTTON) to INPUTS +---@param x number +---@param y number +---@param buttons BUTTON +---@return INPUTS +function buttons_to_inputs(x, y, buttons) end ---@return boolean function toast_visible() end ---@return boolean @@ -2423,7 +2433,7 @@ function Entity:overlaps_with(other) end ---@field set_position fun(self, to_x: number, to_y: number): nil @Set the absolute position of an entity and offset all rendering related things accordingly to teleport without any interpolation or graphical glitches. If the camera is focused on the entity, it is also moved. ---@field process_input fun(self): nil ---@field cutscene CutsceneBehavior - ---@field set_cutscene any @[](Movable&movable + ---@field clear_cutscene any @[](Movable&movable){deletemovable.cutscene_behavior ---@field get_base_behavior fun(self, state_id: integer): VanillaMovableBehavior @Gets a vanilla behavior from this movable, needs to be called before `clear_behaviors`
but the returned values are still valid after a call to `clear_behaviors` ---@field add_behavior fun(self, behavior: MovableBehavior): nil @Add a behavior to this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior` ---@field clear_behavior fun(self, behavior: MovableBehavior): nil @Clear a specific behavior of this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`, a behavior with this behaviors `state_id` may be required to
run this movables statemachine without crashing, so add a new one if you are not sure @@ -2504,8 +2514,6 @@ function Movable:generic_update_world(move, sprint_factor, disable_gravity, on_r ---@class CutsceneBehavior ----@class MovableCutscene : CutsceneBehavior - ---@class PowerupCapable : Movable ---@field remove_powerup fun(self, powerup_type: ENT_TYPE): nil @Removes a currently applied powerup. Specify `ENT_TYPE.ITEM_POWERUP_xxx`, not `ENT_TYPE.ITEM_PICKUP_xxx`! Removing the Eggplant crown does not seem to undo the throwing of eggplants, the other powerups seem to work. ---@field give_powerup fun(self, powerup_type: ENT_TYPE): nil @Gives the player/monster the specified powerup. Specify `ENT_TYPE.ITEM_POWERUP_xxx`, not `ENT_TYPE.ITEM_PICKUP_xxx`! Giving true crown to a monster crashes the game. diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index eb275c7a0..d52c44998 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -1445,6 +1445,15 @@ Load another script by id "author/name" and import its `exports` table. Returns: - `false` if the script was not found but optional is set to true - an error if the script was not found and the optional argument was not set +### inputs_to_buttons + + +> Search script examples for [inputs_to_buttons](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=inputs_to_buttons) + +#### tuple<float, float, [BUTTON](#BUTTON)> inputs_to_buttons([INPUTS](#INPUTS) inputs) + +Converts [INPUTS](#INPUTS) to (x, y, BUTTON) + ### intersection @@ -1857,6 +1866,15 @@ Warp to a level immediately. ## Input functions +### buttons_to_inputs + + +> Search script examples for [buttons_to_inputs](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=buttons_to_inputs) + +#### [INPUTS](#INPUTS) buttons_to_inputs(float x, float y, [BUTTON](#BUTTON) buttons) + +Converts (x, y, BUTTON) to [INPUTS](#INPUTS) + ### get_io diff --git a/docs/src/includes/_types.md b/docs/src/includes/_types.md index d66ea5c59..b56e62e7b 100644 --- a/docs/src/includes/_types.md +++ b/docs/src/includes/_types.md @@ -763,14 +763,6 @@ Type | Name | Description int | [get_state_id()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_state_id) | int | [get_state_id()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_state_id) | Get the `state_id` of a behavior, this is the id that needs to be returned from a behavior's
`get_next_state_id` to enter this state, given that the behavior is added to the movable. -### MovableCutscene - -Derived from [CutsceneBehavior](#CutsceneBehavior) - - -Type | Name | Description ----- | ---- | ----------- - ### PRNG [PRNG](#PRNG) (short for Pseudo-Random-Number-Generator) holds 10 128bit wide buffers of memory that are mutated on every generation of a random number. @@ -6664,7 +6656,7 @@ nil | [reset_gravity()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q nil | [set_position(float to_x, float to_y)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_position) | Set the absolute position of an entity and offset all rendering related things accordingly to teleport without any interpolation or graphical glitches. If the camera is focused on the entity, it is also moved. nil | [process_input()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=process_input) | [CutsceneBehavior](#CutsceneBehavior) | [cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=cutscene) | - | [set_cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_cutscene) | + | [clear_cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=clear_cutscene) | [VanillaMovableBehavior](#VanillaMovableBehavior) | [get_base_behavior(int state_id)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_base_behavior) | Gets a vanilla behavior from this movable, needs to be called before `clear_behaviors`
but the returned values are still valid after a call to `clear_behaviors` nil | [add_behavior(MovableBehavior behavior)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=add_behavior) | Add a behavior to this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior` nil | [clear_behavior(MovableBehavior behavior)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=clear_behavior) | Clear a specific behavior of this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`, a behavior with this behaviors `state_id` may be required to
run this movables statemachine without crashing, so add a new one if you are not sure diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 2343cc2da..3016930aa 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2115,6 +2115,38 @@ end /// Setting to false disables the death screen from popping up for any usual reason, can still load manually lua["set_death_enabled"] = set_death_enabled; + /// Converts INPUTS to (x, y, BUTTON) + lua["inputs_to_buttons"] = [](INPUTS inputs) -> std::tuple + { + float x = 0; + float y = 0; + if (inputs & 0x100) + x = -1; + else if (inputs & 0x200) + x = 1; + if (inputs & 0x400) + y = 1; + else if (inputs & 0x800) + y = -1; + BUTTON buttons = (BUTTON)(inputs & 0x3f); + return std::make_tuple(x, y, buttons); + }; + + /// Converts (x, y, BUTTON) to INPUTS + lua["buttons_to_inputs"] = [](float x, float y, BUTTON buttons) -> INPUTS + { + INPUTS inputs = buttons; + if (x < 0) + inputs |= 0x100; + else if (x > 0) + inputs |= 0x200; + if (y > 0) + inputs |= 0x400; + else if (y < 0) + inputs |= 0x800; + return inputs; + }; + lua.create_named_table("INPUTS", "NONE", 0, "JUMP", 1, "WHIP", 2, "BOMB", 4, "ROPE", 8, "RUN", 16, "DOOR", 32, "MENU", 64, "JOURNAL", 128, "LEFT", 256, "RIGHT", 512, "UP", 1024, "DOWN", 2048); lua.create_named_table( diff --git a/src/game_api/script/usertypes/entity_lua.cpp b/src/game_api/script/usertypes/entity_lua.cpp index 6b354cd36..b693abe30 100644 --- a/src/game_api/script/usertypes/entity_lua.cpp +++ b/src/game_api/script/usertypes/entity_lua.cpp @@ -24,36 +24,6 @@ #include "script/lua_backend.hpp" // for LuaBackend #include "script/safe_cb.hpp" // for make_safe_cb -class CustomCutsceneBehavior : public CutsceneBehavior -{ - public: - using CutsceneCb = bool(Movable*, uint32_t); - CustomCutsceneBehavior(sol::function cb); - ~CustomCutsceneBehavior(); - uint32_t frame{0}; - std::function func; - void update(Movable* movable); -}; - -CustomCutsceneBehavior::CustomCutsceneBehavior(sol::function cb) -{ - func = make_safe_cb(std::move(cb)); -} - -CustomCutsceneBehavior::~CustomCutsceneBehavior() -{ -} - -void CustomCutsceneBehavior::update(Movable* movable) -{ - if (func(movable, ++frame)) - { - game_allocator a; - a.destroy(movable->cutscene_behavior); - movable->cutscene_behavior = nullptr; - } -} - namespace NEntity { void register_usertypes(sol::state& lua) @@ -381,25 +351,13 @@ void register_usertypes(sol::state& lua) movable_type["set_position"] = &Movable::set_position; movable_type["process_input"] = &Movable::process_input; movable_type["cutscene"] = sol::readonly(&Movable::cutscene_behavior); - movable_type["set_cutscene"] = [](Movable& movable, sol::optional cb) + movable_type["clear_cutscene"] = [](Movable& movable) { - if (movable.cutscene_behavior) - { - game_allocator a; - a.destroy(movable.cutscene_behavior); - movable.cutscene_behavior = nullptr; - } - if (cb.has_value()) - { - game_allocator a; - CustomCutsceneBehavior* p = a.allocate(1); - a.construct(p, cb.value()); - movable.cutscene_behavior = p; - } + delete movable.cutscene_behavior; + movable.cutscene_behavior = nullptr; }; lua.new_usertype("CutsceneBehavior", sol::no_constructor); - lua.new_usertype("MovableCutscene", sol::no_constructor, sol::base_classes, sol::bases()); lua["Entity"]["as_entity"] = &Entity::as; lua["Entity"]["as_movable"] = &Entity::as; From 1a100fd3c24000636d2156265928e254ef475a82 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 18:17:29 +0300 Subject: [PATCH 28/29] fix crash when destroying layer with players --- src/game_api/rpc.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index bde6b9124..bd3283623 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -2152,6 +2152,12 @@ void destroy_layer(uint8_t layer) } if (offset != 0) { + auto state = State::get().ptr(); + for (auto i = 0; i < MAX_PLAYERS; ++i) + { + if (state->items->players[i] && state->items->players[i]->layer == layer) + state->items->players[i] = nullptr; + } auto* layer_ptr = State::get().layer(layer); typedef void destroy_func(Layer*); static destroy_func* df = (destroy_func*)(offset); From 56a73228d3f93cab2017d2bb2f45492c7c68338c Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 14 Oct 2023 19:21:04 +0300 Subject: [PATCH 29/29] rename and document death --- docs/game_data/spel2.lua | 4 ++-- docs/src/includes/_globals.md | 18 +++++++++--------- src/game_api/script/lua_vm.cpp | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index e29918e01..ffa664047 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -1249,10 +1249,10 @@ function create_level() end ---@param layer integer ---@return nil function create_layer(layer) end ----Setting to false disables the death screen from popping up for any usual reason, can still load manually +---Setting to false disables all player logic in SCREEN.LEVEL, mainly the death screen from popping up if all players are dead or missing, but also shop camera zoom and some other small things. ---@param enable boolean ---@return nil -function set_death_enabled(enable) end +function set_level_logic_enabled(enable) end ---Converts INPUTS to (x, y, BUTTON) ---@param inputs INPUTS ---@return number, number, BUTTON diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index d52c44998..138da2c7c 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -1619,15 +1619,6 @@ Set the current adventure seed pair Same as `Player.set_heart_color` -### set_death_enabled - - -> Search script examples for [set_death_enabled](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_death_enabled) - -#### nil set_death_enabled(bool enable) - -Setting to false disables the death screen from popping up for any usual reason, can still load manually - ### set_ending_unlock @@ -1690,6 +1681,15 @@ Enables or disables the journal Set the value for the specified config +### set_level_logic_enabled + + +> Search script examples for [set_level_logic_enabled](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_level_logic_enabled) + +#### nil set_level_logic_enabled(bool enable) + +Setting to false disables all player logic in [SCREEN](#SCREEN).LEVEL, mainly the death screen from popping up if all players are dead or missing, but also shop camera zoom and some other small things. + ### set_mask diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 3016930aa..f7a2fb1f9 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2112,8 +2112,8 @@ end /// Initializes an empty layer that doesn't currently exist. lua["create_layer"] = create_layer; - /// Setting to false disables the death screen from popping up for any usual reason, can still load manually - lua["set_death_enabled"] = set_death_enabled; + /// Setting to false disables all player logic in SCREEN.LEVEL, mainly the death screen from popping up if all players are dead or missing, but also shop camera zoom and some other small things. + lua["set_level_logic_enabled"] = set_death_enabled; /// Converts INPUTS to (x, y, BUTTON) lua["inputs_to_buttons"] = [](INPUTS inputs) -> std::tuple