diff --git a/README.md b/README.md index c1eef5f40..1fa4253c1 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ The binaries will be in `build/bin/Release/`. You can also try the scripts in `. ``` --launch_game [path] launch ../Spel2.exe, path/Spel2.exe, or a specific exe, and load OL with Detours +--oldflip launch the game with -oldflip, may improve performance with external windows --console keep console open to debug scripts etc --inject use the old injection method instead of Detours with --launch_game --info_dump output a bunch of game data to 'Spelunky 2/game_data' diff --git a/src/game_api/custom_types.cpp b/src/game_api/custom_types.cpp index f0459dc5e..23fb7ff45 100644 --- a/src/game_api/custom_types.cpp +++ b/src/game_api/custom_types.cpp @@ -551,7 +551,8 @@ std::span get_custom_entity_types(CUSTOM_TYPE type) "ENT_TYPE_ITEM_DMCRATE", "ENT_TYPE_ITEM_PRESENT", "ENT_TYPE_ITEM_GHIST_PRESENT", - "ENT_TYPE_ITEM_ALIVE_EMBEDDED_ON_ICE"); + "ENT_TYPE_ITEM_ALIVE_EMBEDDED_ON_ICE", + "ENT_TYPE_ITEM_POT"); case CUSTOM_TYPE::CONVEYORBELT: return make_custom_entity_type_list( "ENT_TYPE_FLOOR_CONVEYORBELT_LEFT", diff --git a/src/game_api/entities_logical.hpp b/src/game_api/entities_logical.hpp index 3d5610fd4..4be0598cd 100644 --- a/src/game_api/entities_logical.hpp +++ b/src/game_api/entities_logical.hpp @@ -30,10 +30,13 @@ class ShootingStarSpawner : public Entity class LogicalDoor : public Entity { public: + /// Spawns this entity when not covered by floor. Must be initialized to valid ENT_TYPE before revealed, or crashes the game. ENT_TYPE door_type; - ENT_TYPE platform_type; // always 37? yeah, that's the floor platform... + /// Spawns this entity below when tile below is uncovered. Doesn't spawn anything if it was never covered by floor, unless platform_spawned is set to false. Must be initialized to valid ENT_TYPE before revealed, or crashes the game. + ENT_TYPE platform_type; + /// Set automatically when not covered by floor. bool not_hidden; - /// Is set true when you bomb the door, no matter what door, can't be reset + /// Set automatically when tile below is not covered by floor. Unset to force the platform to spawn if it was never covered in the first place. bool platform_spawned; bool unk5; bool unk6; diff --git a/src/game_api/flags.hpp b/src/game_api/flags.hpp index 88b31f807..79aaa2c53 100644 --- a/src/game_api/flags.hpp +++ b/src/game_api/flags.hpp @@ -754,4 +754,5 @@ const char* pause_types[]{ "8: ?", "16: ?", "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 }; diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 0b53eda97..4c197abe8 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -2058,3 +2058,67 @@ void set_boss_door_control_enabled(bool enable) else recover_mem("set_boss_door_control_enabled"); } + +void update_state() +{ + static size_t offset = 0; + if (offset == 0) + { + offset = get_address("state_refresh"); + } + if (offset != 0) + { + auto state = State::get().ptr(); + typedef void refresh_func(StateMemory*); + static refresh_func* rf = (refresh_func*)(offset); + rf(state); + } +} + +void set_frametime(std::optional frametime) +{ + static size_t offset = 0; + if (offset == 0) + offset = get_address("engine_frametime"); + if (offset != 0) + { + if (frametime.has_value()) + write_mem_recoverable("engine_frametime", offset, frametime.value(), true); + else + recover_mem("engine_frametime"); + } +} + +std::optional get_frametime() +{ + static size_t offset = 0; + if (offset == 0) + offset = get_address("engine_frametime"); + if (offset != 0) + return memory_read(offset); + return std::nullopt; +} + +void set_frametime_inactive(std::optional frametime) +{ + static size_t offset = 0; + if (offset == 0) + offset = get_address("engine_frametime") + 0x10; + if (offset != 0) + { + if (frametime.has_value()) + write_mem_recoverable("engine_frametime_inactive", offset, frametime.value(), true); + else + recover_mem("engine_frametime_inactive"); + } +} + +std::optional get_frametime_inactive() +{ + static size_t offset = 0; + if (offset == 0) + offset = get_address("engine_frametime") + 0x10; + if (offset != 0) + return memory_read(offset); + return std::nullopt; +} diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index 2b6c155fa..91ac1f8e8 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -143,3 +143,8 @@ void activate_tiamat_position_hack(bool activate); void activate_crush_elevator_hack(bool activate); void activate_hundun_hack(bool activate); void set_boss_door_control_enabled(bool enable); +void update_state(); +void set_frametime(std::optional frametime); +std::optional get_frametime(); +void set_frametime_inactive(std::optional frametime); +std::optional get_frametime_inactive(); diff --git a/src/game_api/script.cpp b/src/game_api/script.cpp index 61fe81965..40790dadf 100644 --- a/src/game_api/script.cpp +++ b/src/game_api/script.cpp @@ -128,3 +128,7 @@ void SpelunkyScript::render_options() { m_Impl->Lock()->render_options(); } +std::string SpelunkyScript::execute(std::string code) +{ + return m_Impl->Lock()->execute(code); +} diff --git a/src/game_api/script.hpp b/src/game_api/script.hpp index 6a993f5be..889f7bb82 100644 --- a/src/game_api/script.hpp +++ b/src/game_api/script.hpp @@ -98,6 +98,8 @@ class SpelunkyScript void draw(ImDrawList* dl); void render_options(); + std::string execute(std::string code); + private: std::unique_ptr m_Impl; }; diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 34004f314..6f9bd4ca1 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2048,24 +2048,24 @@ end return AABB(ax + index * w + 0.02f * f, ay, ax + index * w + w - 0.02f * f, ay - h); }; + /// Olmec cutscene moves Olmec and destroys the four floor tiles, so those things never happen if the cutscene is disabled, and Olmec will spawn on even ground. More useful for level gen mods, where the cutscene doesn't make sense. You can also set olmec_cutscene.timer to the last frame (809) to skip to the end, with Olmec in the hole. lua["set_olmec_cutscene_enabled"] = set_olmec_cutscene_enabled; - /// Tiamat cutscene is also responsible for locking the exit door - /// So you may need to close it yourself if you still want to be required to kill Tiamat + /// Tiamat cutscene is also responsible for locking the exit door, so you may need to close it yourself if you still want Tiamat kill to be required lua["set_tiamat_cutscene_enabled"] = set_tiamat_cutscene_enabled; /// Activate custom variables for position used for detecting the player (normally hardcoded) - /// note: because those variables are custom and game does not initiate them, you need to do it yourself for each Tiamat entity, recommending `set_post_entity_spawn` + /// note: because those variables are custom and game does not initiate them, you need to do it yourself for each Tiamat entity, recommending set_post_entity_spawn /// default game values are: attack_x = 17.5 attack_y = 62.5 lua["activate_tiamat_position_hack"] = activate_tiamat_position_hack; /// Activate custom variables for speed and y coordinate limit for crushing elevator - /// note: because those variables are custom and game does not initiate them, you need to do it yourself for each CrushElevator entity, recommending `set_post_entity_spawn` + /// note: because those variables are custom and game does not initiate them, you need to do it yourself for each CrushElevator entity, recommending set_post_entity_spawn /// default game values are: speed = 0.0125, y_limit = 98.5 lua["activate_crush_elevator_hack"] = activate_crush_elevator_hack; /// Activate custom variables for y coordinate limit for hundun and spawn of it's heads - /// note: because those variables are custom and game does not initiate them, you need to do it yourself for each Hundun entity, recommending `set_post_entity_spawn` + /// note: because those variables are custom and game does not initiate them, you need to do it yourself for each Hundun entity, recommending set_post_entity_spawn /// default game value are: y_limit = 98.5, rising_speed_x = 0, rising_speed_y = 0.0125, bird_head_spawn_y = 55, snake_head_spawn_y = 71 lua["activate_hundun_hack"] = activate_hundun_hack; @@ -2073,6 +2073,21 @@ end /// This will also prevent game crashing when there is no exit door when they are in level lua["set_boss_door_control_enabled"] = set_boss_door_control_enabled; + /// Run state update manually, i.e. simulate one logic frame. Use in e.g. POST_UPDATE, but be mindful of infinite loops, this will cause another POST_UPDATE. Can even be called thousands of times to simulate minutes of gameplay in a few seconds. + lua["update_state"] = update_state; + + /// Set engine target frametime (1/framerate, default 1/60). Always capped by your GPU max FPS / VSync. To run the engine faster than rendered FPS, try update_state. Set to 0 to go as fast as possible. Call without arguments to reset. + lua["set_frametime"] = set_frametime; + + /// Get engine target frametime (1/framerate, default 1/60). + lua["get_frametime"] = get_frametime; + + /// Set engine target frametime when game is unfocused (1/framerate, default 1/33). Always capped by the engine frametime. Set to 0 to go as fast as possible. Call without arguments to reset. + lua["set_frametime_unfocused"] = set_frametime_inactive; + + /// Get engine target frametime when game is unfocused (1/framerate, default 1/33). + lua["get_frametime_unfocused"] = get_frametime_inactive; + 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/script_impl.cpp b/src/game_api/script/script_impl.cpp index a1663b31f..dfd058b6e 100644 --- a/src/game_api/script/script_impl.cpp +++ b/src/game_api/script/script_impl.cpp @@ -207,3 +207,36 @@ const std::filesystem::path& ScriptImpl::get_root_path() const { return script_folder; } + +std::string ScriptImpl::execute(std::string str) +{ + if (!code.starts_with("return")) + { + std::string ret = execute_raw("return " + str); + if (!ret.starts_with("sol: ")) + { + return ret; + } + } + return execute_raw(std::move(str)); +} +std::string ScriptImpl::execute_raw(std::string str) +{ + try + { + auto ret = execute_lua(lua, str); + if (ret.get_type() == sol::type::nil || ret.get_type() == sol::type::none) + { + return ""; + } + else + { + sol::function serpent = lua["serpent"]["block"]; + return serpent(ret); + } + } + catch (const sol::error& e) + { + return e.what(); + } +} diff --git a/src/game_api/script/script_impl.hpp b/src/game_api/script/script_impl.hpp index f2373960e..85a11ada4 100644 --- a/src/game_api/script/script_impl.hpp +++ b/src/game_api/script/script_impl.hpp @@ -44,4 +44,7 @@ class ScriptImpl : public LockableLuaBackend virtual const char* get_path() const override; virtual const char* get_root() const override; virtual const std::filesystem::path& get_root_path() const override; + + std::string execute(std::string str); + std::string execute_raw(std::string str); }; diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 1923bec9a..f27008208 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -1840,6 +1840,19 @@ std::unordered_map g_address_rules{ .at_exe() .function_start(), }, + { + /* it's a static double, just find something that reads it + this+0x08 is clearly some kind of framerate related double, cause it's 60, but don't know what it does + this+0x10 is hopefully unfocused frametime for the other function, but maybe it needs own pattern + 22d12248 00 00 00 double 0.01666666753590107 + 20 11 11 + 91 3f */ + "engine_frametime"sv, + PatternCommandBuffer{} + .find_after_inst("48 8d 04 0a 48 85 d2 48 0f 44 c2 48 85 c9 48 0f 44 c1 66 0f 28 c8"_gh) + .decode_pc(4) + .at_exe(), + }, { // Borrowed from Playlunky logger.cpp "game_log_function"sv, diff --git a/src/game_api/window_api.cpp b/src/game_api/window_api.cpp index 43c4b4265..4ecfb1679 100644 --- a/src/game_api/window_api.cpp +++ b/src/game_api/window_api.cpp @@ -24,6 +24,7 @@ bool detect_wine() return true; } +UINT g_SyncInterval{1}; IDXGISwapChain* g_SwapChain{nullptr}; ID3D11Device* g_Device{nullptr}; ID3D11DeviceContext* g_Context{nullptr}; @@ -210,6 +211,8 @@ LRESULT CALLBACK hkKeyboard(const int code, const WPARAM wParam, const LPARAM lP static bool skip_hkPresent = false; HRESULT STDMETHODCALLTYPE hkPresent(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags) { + SyncInterval = g_SyncInterval; + if (skip_hkPresent) return g_OrigSwapChainPresent(pSwapChain, SyncInterval, Flags); @@ -468,6 +471,11 @@ void hide_cursor() } } +void imgui_vsync(bool enable) +{ + g_SyncInterval = (UINT)enable; +} + ID3D11Device* get_device() { return g_Device; diff --git a/src/game_api/window_api.hpp b/src/game_api/window_api.hpp index acb5c7884..249f2e8b7 100644 --- a/src/game_api/window_api.hpp +++ b/src/game_api/window_api.hpp @@ -33,5 +33,6 @@ HWND get_window(); void show_cursor(); void hide_cursor(); +void imgui_vsync(bool enable); struct ID3D11Device* get_device(); diff --git a/src/injected/main.cpp b/src/injected/main.cpp index 143ee6d74..569a15f4e 100644 --- a/src/injected/main.cpp +++ b/src/injected/main.cpp @@ -99,7 +99,7 @@ void attach_stdout(DWORD pid) freopen_s(&stream, "CONOUT$", "w", stdout); freopen_s(&stream, "CONOUT$", "w", stderr); // freopen_s(&stream, "CONIN$", "r", stdin); - INFO("Press Ctrl+C to detach this window from the process."); + INFO("Do not close this window or the game will also die. Press Ctrl+C to detach this window from the game process."); } void run() diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 18c1f8599..ab3bda02f 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -95,6 +95,7 @@ std::map default_keys{ {"toggle_disable_pause", OL_KEY_CTRL | OL_KEY_SHIFT | 'P'}, {"toggle_grid", OL_KEY_CTRL | OL_KEY_SHIFT | 'G'}, {"toggle_hitboxes", OL_KEY_CTRL | OL_KEY_SHIFT | 'H'}, + {"toggle_entityinfo", OL_KEY_CTRL | OL_KEY_SHIFT | 'E'}, {"toggle_hud", OL_KEY_CTRL | 'H'}, {"toggle_lights", OL_KEY_CTRL | 'L'}, {"toggle_ghost", OL_KEY_CTRL | 'O'}, @@ -265,7 +266,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; + 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; 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; @@ -294,7 +295,7 @@ std::string fontfile = ""; std::vector fontsize = {14.0f, 32.0f, 72.0f}; [[maybe_unused]] const char s8_zero = 0, s8_one = 1, s8_min = -128, s8_max = 127; -[[maybe_unused]] const ImU8 u8_zero = 0, u8_one = 1, u8_min = 0, u8_max = 255, u8_four = 4, u8_seven = 7, u8_seventeen = 17, u8_draw_depth_max = 53; +[[maybe_unused]] const ImU8 u8_zero = 0, u8_one = 1, u8_min = 0, u8_max = 255, u8_four = 4, u8_seven = 7, u8_seventeen = 17, u8_draw_depth_max = 52, u8_shader_max = 36; [[maybe_unused]] const short s16_zero = 0, s16_one = 1, s16_min = -32768, s16_max = 32767; [[maybe_unused]] const ImU16 u16_zero = 0, u16_one = 1, u16_min = 0, u16_max = 65535; [[maybe_unused]] const ImS32 s32_zero = 0, s32_one = 1, s32_min = INT_MIN / 2, s32_max = INT_MAX / 2, s32_hi_a = INT_MAX / 2 - 100, s32_hi_b = INT_MAX / 2; @@ -327,6 +328,7 @@ std::map options = { {"disable_pause", false}, {"draw_grid", false}, {"draw_hitboxes", false}, + {"draw_entityinfo", false}, {"enable_unsafe_scripts", false}, {"warp_increments_level_count", true}, {"warp_transition", false}, @@ -348,7 +350,11 @@ std::map options = { {"hd_cursor", true}, {"inverted", false}, {"borders", false}, - {"console_alt_keys", false}}; + {"console_alt_keys", false}, + {"vsync", true}}; + +double g_engine_fps = 60.0, g_unfocused_fps = 33.0; +double fps_min = 0, fps_max = 600.0; bool g_speedhack_hooked = false; float g_speedhack_multiplier = 1.0; @@ -1042,6 +1048,7 @@ void load_config(std::string file) } ImGui::GetIO().ConfigDockingWithShift = options["docking_with_shift"]; g_Console->set_alt_keys(options["console_alt_keys"]); + imgui_vsync(options["vsync"]); save_config(file); } @@ -1304,6 +1311,11 @@ std::string spawned_type() int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true) { + static const ENT_TYPE also_snap[] = { + to_id("ENT_TYPE_LOGICAL_DOOR"), + to_id("ENT_TYPE_LOGICAL_BLACKMARKET_DOOR"), + to_id("ENT_TYPE_LOGICAL_PLATFORM_SPAWNER"), + }; bool flip = g_vx < -0.04f; std::pair cpos = UI::click_position(g_x, g_y); if (to_spawn.name.find("ENT_TYPE_CHAR") != std::string::npos) @@ -1330,7 +1342,11 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true) else if (to_spawn.name.find("ENT_TYPE_LIQUID") == std::string::npos) { bool snap = options["snap_to_grid"]; - if (to_spawn.name.find("ENT_TYPE_FLOOR") != std::string::npos) + if (std::find(std::begin(also_snap), std::end(also_snap), to_spawn.id) != std::end(also_snap)) + { + snap = true; + } + else if (to_spawn.name.find("ENT_TYPE_FLOOR") != std::string::npos) { snap = true; g_vx = 0; @@ -1381,12 +1397,18 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true) callbacks.push_back(cb); } } - else if (flip) + if (flip) { auto ent = get_entity_ptr(spawned); if (ent) ent->flags |= (1U << 16); } + if (to_spawn.id == also_snap[0]) + { + auto ent = get_entity_ptr(spawned)->as(); + 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) g_last_id = spawned; return spawned; @@ -2069,12 +2091,55 @@ void force_kits() } } +bool toggle_pause() +{ + g_pause_at = -1; + g_pause_time = -1; + if (g_pause_type & 0x40) + { + if (!paused) + { + g_pause_time = g_state->time_startup; + pause_updates = true; + paused = true; + } + else + { + g_pause_time = -1; + pause_updates = false; + paused = false; + } + g_ui_scripts["pause"]->execute(fmt::format("pause_at = {}", g_pause_time)); + } + else + { + if (g_state->pause == 0) + { + g_state->pause = (uint8_t)g_pause_type; + paused = true; + } + else + { + g_state->pause = 0; + paused = false; + } + } + return paused; +} + void frame_advance() { - if (g_state->pause == 0 && g_pause_at != -1 && (unsigned)g_pause_at <= UI::get_frame_count()) + if (g_pause_type & 0x40) { - g_state->pause = (uint8_t)g_pause_type; - g_pause_at = -1; + g_ui_scripts["pause"]->execute(fmt::format("pause_at = {}", g_pause_time)); + } + else + { + if (g_state->pause == 0 && g_pause_at != -1 && (unsigned)g_pause_at <= UI::get_frame_count()) + { + g_state->pause = (uint8_t)g_pause_type; + g_pause_at = -1; + } } } @@ -2795,6 +2860,10 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) { options["draw_hitboxes"] = !options["draw_hitboxes"]; } + else if (pressed("toggle_entityinfo", wParam)) + { + options["draw_entityinfo"] = !options["draw_entityinfo"]; + } else if (pressed("toggle_grid", wParam)) { options["draw_grid"] = !options["draw_grid"]; @@ -2813,17 +2882,7 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) } else if (pressed("toggle_pause", wParam)) { - g_pause_at = -1; - if (g_state->pause == 0) - { - g_state->pause = (uint8_t)g_pause_type; - paused = true; - } - else - { - g_state->pause = 0; - paused = false; - } + toggle_pause(); } else if (pressed("toggle_hud", wParam)) { @@ -2835,10 +2894,21 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) } else if (pressed("frame_advance", wParam) || pressed("frame_advance_alt", wParam)) { - if (g_state->pause == (uint8_t)g_pause_type) + if (g_pause_type & 0x40) { - g_pause_at = UI::get_frame_count() + 1; - g_state->pause = 0; + if (pause_updates) + { + g_pause_time = g_state->time_startup + 1; + g_ui_scripts["pause"]->execute(fmt::format("pause_at = {}", g_pause_time)); + } + } + else + { + if (g_state->pause == (uint8_t)g_pause_type) + { + g_pause_at = UI::get_frame_count() + 1; + g_state->pause = 0; + } } } else if (pressed("toggle_disable_pause", wParam)) @@ -3997,7 +4067,7 @@ void render_grid(ImColor gridcolor = ImColor(1.0f, 1.0f, 1.0f, 0.2f)) { for (unsigned int y = 0; y < g_state->h; ++y) { - auto room_temp = UI::get_room_template(x, y, g_state->camera_layer); + auto room_temp = UI::get_room_template(x, y, peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer); if (room_temp.has_value()) { auto room_name = UI::get_room_template_name(room_temp.value()); @@ -4175,7 +4245,46 @@ void render_path() draw_list->PathStroke(color, 0, thick); } -void render_hitbox(Entity* ent, bool cross, ImColor color, bool filled = false) +std::string entity_tooltip(Entity* hovered) +{ + std::string coords; + coords += fmt::format("{}, {} ({:.2f}, {:.2f})", hovered->uid, entity_names[hovered->type->id], hovered->abs_x == -FLT_MAX ? hovered->x : hovered->abs_x, hovered->abs_y == -FLT_MAX ? hovered->y : hovered->abs_y); + if (hovered->type->id == to_id("ENT_TYPE_ITEM_POT") && hovered->as()->inside > 0) + coords += fmt::format(" ({})", entity_names[hovered->as()->inside]); + else if (hovered->type->id == to_id("ENT_TYPE_ITEM_PRESENT") || hovered->type->id == to_id("ENT_TYPE_ITEM_GHIST_PRESENT")) + coords += fmt::format(" ({})", entity_names[hovered->as()->inside]); + else if (hovered->type->id == to_id("ENT_TYPE_ITEM_COFFIN")) + coords += fmt::format(" ({})", entity_names[hovered->as()->inside]); + else if (hovered->type->id == to_id("ENT_TYPE_ITEM_CRATE") || hovered->type->id == to_id("ENT_TYPE_ITEM_DMCRATE") || hovered->type->id == to_id("ENT_TYPE_ITEM_ALIVE_EMBEDDED_ON_ICE")) + coords += fmt::format(" ({})", entity_names[hovered->as()->inside]); + else if (hovered->type->id == to_id("ENT_TYPE_ITEM_CHEST")) + { + auto chest = hovered->as(); + if (chest->bomb) + coords += " (BOMB)"; + if (chest->leprechaun) + coords += " (LEPRECHAUN)"; + } + else if (hovered->type->search_flags & 7) + { + auto ent = hovered->as(); + coords += fmt::format(" ({} HP)", ent->health); + } + if (hovered->overlay) + { + coords += fmt::format("\nON: {}, {} ({:.2f}, {:.2f})", hovered->overlay->uid, entity_names[hovered->overlay->type->id], hovered->overlay->abs_x == -FLT_MAX ? hovered->overlay->x : hovered->overlay->abs_x, hovered->overlay->abs_y == -FLT_MAX ? hovered->overlay->y : hovered->overlay->abs_y); + } + if (hovered->type->search_flags & 15 && hovered->as()->last_owner_uid > -1) + { + auto ent = hovered->as(); + auto owner = get_entity_ptr(ent->last_owner_uid); + if (owner) + coords += fmt::format("\nOWNER: {}, {} ({:.2f}, {:.2f})", owner->uid, entity_names[owner->type->id], owner->abs_x == -FLT_MAX ? owner->x : owner->abs_x, owner->abs_y == -FLT_MAX ? owner->y : owner->abs_y); + } + return coords; +} + +void render_hitbox(Entity* ent, bool cross, ImColor color, bool filled = false, bool rounded = false) { const auto type = ent->type->id; if (!type) @@ -4191,8 +4300,8 @@ void render_hitbox(Entity* ent, bool cross, ImColor color, bool filled = false) UI::screen_position(render_position.first + ent->hitboxx + ent->offsetx, render_position.second + ent->hitboxy + ent->offsety); ImVec2 sorigin = screenify({originx, originy}); ImVec2 spos = screenify({(boxa_x + boxb_x) / 2, (boxa_y + boxb_y) / 2}); - ImVec2 sboxa = screenify({boxa_x, boxa_y}); - ImVec2 sboxb = screenify({boxb_x, boxb_y}); + ImVec2 sboxa = screenify({boxa_x, boxb_y}); + ImVec2 sboxb = screenify({boxb_x, boxa_y}); auto* draw_list = ImGui::GetWindowDrawList(); if (cross) { @@ -4218,9 +4327,12 @@ void render_hitbox(Entity* ent, bool cross, ImColor color, bool filled = false) else draw_list->AddCircle(fix_pos(spos), sboxb.x - spos.x, color, 0, 2.0f); else if (filled) - draw_list->AddRectFilled(fix_pos(sboxa), fix_pos(sboxb), color); + draw_list->AddRectFilled(fix_pos(sboxa), fix_pos(sboxb), color, rounded ? 5.0f : 0.0f); else - draw_list->AddRect(fix_pos(sboxa), fix_pos(sboxb), color, 0.0f, 0, 2.0f); + draw_list->AddRect(fix_pos(sboxa), fix_pos(sboxb), color, rounded ? 5.0f : 0.0f, 0, 2.0f); + + if (options["draw_entityinfo"] && !cross && !filled) + draw_list->AddText(fix_pos(ImVec2(sboxa.x, sboxb.y)), ImColor(1.0f, 1.0f, 1.0f, 0.8f), entity_tooltip(ent).c_str()); if ((g_hitbox_mask & 0x8000) == 0) return; @@ -4496,7 +4608,7 @@ void render_clickhandler() if (options["draw_hitboxes"] && g_state->screen != 5) { static const auto olmec = to_id("ENT_TYPE_ACTIVEFLOOR_OLMEC"); - for (auto entity : UI::get_entities_by({}, g_hitbox_mask, (LAYER)g_state->camera_layer)) + for (auto entity : UI::get_entities_by({}, g_hitbox_mask, (LAYER)(peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer))) { auto ent = get_entity_ptr(entity); if (!ent) @@ -4511,7 +4623,8 @@ void render_clickhandler() if (!UI::has_active_render(ent) && (ent->type->search_flags & 0x7000) == 0) continue; - render_hitbox(ent, false, ImColor(0, 255, 255, 150)); + if ((ent->type->search_flags & 1) == 0 || ent->as()->ai) + render_hitbox(ent, false, ImColor(0, 255, 255, 150)); } if ((g_hitbox_mask & 0x1) != 0) { @@ -4546,16 +4659,21 @@ void render_clickhandler() to_id("ENT_TYPE_FLOOR_TELEPORTINGBORDER"), to_id("ENT_TYPE_FLOOR_SPIKES"), }; - for (auto entity : UI::get_entities_by(additional_fixed_entities, 0x180, LAYER::PLAYER)) // FLOOR | ACTIVEFLOOR + for (auto entity : UI::get_entities_by(additional_fixed_entities, 0x180, (LAYER)(peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer))) // FLOOR | ACTIVEFLOOR { auto ent = get_entity_ptr(entity); render_hitbox(ent, false, ImColor(0, 255, 255, 150)); } - for (auto entity : UI::get_entities_by({(ENT_TYPE)CUSTOM_TYPE::TRIGGER}, 0x1000, LAYER::PLAYER)) // LOGICAL + for (auto entity : UI::get_entities_by({(ENT_TYPE)CUSTOM_TYPE::TRIGGER}, 0x1000, (LAYER)(peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer))) // LOGICAL { auto ent = get_entity_ptr(entity); render_hitbox(ent, false, ImColor(255, 0, 0, 150)); } + for (auto entity : UI::get_entities_by({to_id("ENT_TYPE_LOGICAL_DOOR")}, 0x1000, (LAYER)(peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer))) // DOOR + { + auto ent = get_entity_ptr(entity); + render_hitbox(ent, false, ImColor(255, 180, 45, 150), false, true); + } // OOB kill bounds auto* draw_list = ImGui::GetBackgroundDrawList(); @@ -4584,7 +4702,7 @@ void render_clickhandler() if (hovered != nullptr) { render_hitbox(hovered, true, ImColor(50, 50, 255, 200)); - coords += fmt::format("\n{}, {}", hovered->uid, entity_names[hovered->type->id]); + coords += "\n" + entity_tooltip(hovered); } ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4.0f, 4.0f}); tooltip(coords.c_str(), true); @@ -4603,7 +4721,7 @@ void render_clickhandler() auto ent = get_entity_ptr(entity); if (ent) { - if (ent->layer == g_state->camera_layer) + if (ent->layer == (peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer)) render_hitbox(ent, false, front_col, true); else render_hitbox(ent, false, back_col, true); @@ -5375,6 +5493,7 @@ void render_options() hook_savegame(); } tooltip("Enable this if you want to keep your\nsave game unaffected by tomfoolery."); + if (ImGui::SliderFloat("Speedhack##SpeedHack", &g_speedhack_multiplier, 0.1f, 5.f)) { if (should_speedhack()) @@ -5384,6 +5503,27 @@ void render_options() tooltip("Slow down or speed up everything,\nlike in Cheat Engine.", "speedhack_decrease"); ImGui::Checkbox("Fast menus and transitions##SpeedHackMenu", &options["speedhack"]); tooltip("Enable 10x speedhack automatically when not controlling a character.", "toggle_speedhack_auto"); + + if (ImGui::SliderScalar("Engine FPS##EngineFPS", ImGuiDataType_Double, &g_engine_fps, &fps_min, &fps_max, "%f")) + g_Console.get()->execute(fmt::format("set_frametime({})", g_engine_fps == 0 ? 0 : 1.0 / g_engine_fps)); + tooltip("Set target engine FPS. Always capped by max GPU FPS.\n0 = as fast as it can go."); + ImGui::SameLine(); + if (ImGui::Button("Reset##ResetFPS")) + { + g_engine_fps = 60.0; + g_Console.get()->execute("set_frametime()"); + } + + if (ImGui::SliderScalar("Unfocused FPS##UnfocusedFPS", ImGuiDataType_Double, &g_unfocused_fps, &fps_min, &fps_max, "%f")) + g_Console.get()->execute(fmt::format("return set_frametime_unfocused({})", g_unfocused_fps == 0 ? 0 : 1.0 / g_engine_fps)); + tooltip("Set target unfocused FPS. Always capped by max Engine FPS.\n0 = as fast as it can go."); + ImGui::SameLine(); + if (ImGui::Button("Reset##ResetUnfocusedFPS")) + { + g_unfocused_fps = 33.0; + g_Console.get()->execute("set_frametime_unfocused()"); + } + bool void_mode = g_ui_scripts["void"]->is_enabled(); if (ImGui::Checkbox("Void sandbox mode", &void_mode)) { @@ -5423,10 +5563,12 @@ void render_options() ImGui::Checkbox("Spawn floor decorated##Decorate", &options["spawn_floor_decorated"]); tooltip("Add decorations to spawned floor."); ImGui::Checkbox("Draw hitboxes##DrawEntityBox", &options["draw_hitboxes"]); - tooltip("Draw hitboxes for all movable and hovered entities.", "toggle_hitboxes"); + tooltip("Draw hitboxes for all movable and hovered entities. Also mouse tooltips.", "toggle_hitboxes"); ImGui::SameLine(); ImGui::Checkbox("interpolated##DrawRealBox", &options["draw_hitboxes_interpolated"]); tooltip("Use interpolated render position for smoother hitboxes on hifps.\nActual game logic is not interpolated like this though."); + ImGui::Checkbox("Draw entity info##DrawEntityInfo", &options["draw_entityinfo"]); + tooltip("Draw entity names, uids and some random stuff next to the hitboxes.", "toggle_entityinfo"); ImGui::Checkbox("Smooth camera dragging", &options["smooth_camera"]); tooltip("Smooth camera movement when dragging, unless paused."); ImGui::SliderFloat("Camera speed##DragSpeed", &g_camera_speed, 1.0f, 5.0f); @@ -5446,7 +5588,11 @@ void render_options() else ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable; } - tooltip("Allow dragging tools outside the main game window, to different monitor etc."); + tooltip("Allow dragging tools outside the main game window, to different monitor etc.\nMay work smoother with the -oldflip game command line switch."); + + if (ImGui::Checkbox("Enable vsync", &options["vsync"])) + imgui_vsync(options["vsync"]); + tooltip("Disabling game vsync may affect performance with external windows,\nfor better or worse."); if (ImGui::Checkbox("Docking only while holding Shift", &options["docking_with_shift"])) { @@ -5508,7 +5654,7 @@ void render_options() if (submenu("Frame advance / Engine pause type")) { ImGui::PushID("PauseType"); - for (int i = 1; i < 6; i++) + for (int i = 1; i < 7; i++) { ImGui::CheckboxFlags(pause_types[i], &g_pause_type, (int)std::pow(2, i)); } @@ -6778,7 +6924,7 @@ void render_entity_props(int uid, bool detached = false) ImGui::InputFloat("Absolute X##EntityAbsoluteX", &entity->abs_x, 0.2f, 1.0f); ImGui::InputFloat("Absolute Y##EntityAbsoluteY", &entity->abs_y, 0.2f, 1.0f); ImGui::InputFloat("Velocity X##EntityVelocityX", &movable->velocityx, 0.2f, 1.0f); - ImGui::InputFloat("Velocity y##EntityVelocityY", &movable->velocityy, 0.2f, 1.0f); + ImGui::InputFloat("Velocity Y##EntityVelocityY", &movable->velocityy, 0.2f, 1.0f); } ImGui::InputFloat("Angle##EntityAngle", &entity->angle, 0.2f, 1.0f); if (is_movable) @@ -6901,6 +7047,20 @@ void render_entity_props(int uid, bool detached = false) ImGui::SameLine(); ImGui::Text("%s", theme_name(target->theme)); } + else if (entity_type == to_id("ENT_TYPE_LOGICAL_DOOR")) + { + auto door = entity->as(); + ImGui::Text("Door type:"); + ImGui::InputInt("##LogicalDoorDoor", (int*)&door->door_type, 1, 10); + ImGui::SameLine(); + ImGui::Text("%s", entity_names[door->door_type].c_str()); + ImGui::Text("Platform type:"); + ImGui::InputInt("##LogicalDoorPlatform", (int*)&door->platform_type, 1, 10); + ImGui::SameLine(); + ImGui::Text("%s", entity_names[door->platform_type].c_str()); + ImGui::Checkbox("Door spawned##LogicalDoorSpawned", &door->not_hidden); + ImGui::Checkbox("Platform spawned##LogicalDoorPlatformSpawned", &door->platform_spawned); + } else if (entity->type->search_flags & 0x7) // PLYAER, MOUNT, MONSTER { auto entity_pow = entity->as(); @@ -7009,7 +7169,8 @@ void render_entity_props(int uid, bool detached = false) uint8_t draw_depth = entity->draw_depth; if (ImGui::DragScalar("Draw depth##EntityDrawDepth", ImGuiDataType_U8, &draw_depth, 0.2f, &u8_zero, &u8_draw_depth_max)) entity->set_draw_depth(draw_depth); - + if (entity->rendering_info) + ImGui::DragScalar("Shader##EntityShader", ImGuiDataType_U8, &entity->rendering_info->shader, 0.2f, &u8_zero, &u8_shader_max); ImGui::DragScalar("Animation frame##EntityAnimationFrame", ImGuiDataType_U16, &entity->animation_frame, 0.2f, &u16_zero, &u16_max); ImGui::InputText(fmt::format("Texture: {}##EntityTexture", textureid).c_str(), &texture, ImGuiInputTextFlags_ReadOnly); // ImGui::InputText("Texture path##EntityTexturePath", &texturepath, ImGuiInputTextFlags_ReadOnly); @@ -7517,12 +7678,7 @@ void render_game_props() gamestate += "Pause "; ImGui::LabelText("Game state", "%s", gamestate.c_str()); if (ImGui::Checkbox("Pause game engine##PauseSim", &paused)) - { - if (paused) - g_state->pause = (uint8_t)g_pause_type; - else - g_state->pause = 0; - } + toggle_pause(); tooltip("Pause time while still being able to teleport, spawn and move entities", "toggle_pause"); endmenu(); } @@ -8237,13 +8393,27 @@ AABB player_hud_position(int p = 0) void render_prohud() { + static float engine_fps = 0; + static std::chrono::time_point last_time; + static uint32_t last_frame; + auto this_frame = UI::get_frame_count(); + auto frame_delta = this_frame - last_frame; + auto this_time = std::chrono::system_clock::now(); + auto time_delta = std::chrono::duration(this_time - last_time); + if (time_delta.count() > 1.0f) + { + engine_fps = static_cast(frame_delta) / time_delta.count(); + last_frame = this_frame; + last_time = this_time; + } + auto io = ImGui::GetIO(); auto base = ImGui::GetMainViewport(); ImDrawList* dl = ImGui::GetBackgroundDrawList(base); auto topmargin = 0.0f; if (options["menu_ui"] && !hide_ui) topmargin = ImGui::GetTextLineHeight(); - std::string buf = fmt::format("TIMER:{}/{} FRAME:{:#06} TOTAL:{:#06} LEVEL:{:#06} COUNT:{} SCREEN:{} SIZE:{}x{} PAUSE:{} FPS:{:.0f}", format_time(g_state->time_level), format_time(g_state->time_total), UI::get_frame_count(), g_state->time_total, g_state->time_level, g_state->level_count, g_state->screen, g_state->w, g_state->h, g_state->pause, io.Framerate); + std::string buf = fmt::format("TIMER:{}/{} FRAME:{:#06} START:{:#06} TOTAL:{:#06} LEVEL:{:#06} COUNT:{} SCREEN:{} SIZE:{}x{} PAUSE:{} FPS:{:.2f} ENGINE:{:.2f} TARGET:{:.2f}", format_time(g_state->time_level), format_time(g_state->time_total), UI::get_frame_count(), g_state->time_startup, g_state->time_total, g_state->time_level, g_state->level_count, g_state->screen, g_state->w, g_state->h, g_state->pause, io.Framerate, engine_fps, g_engine_fps); ImVec2 textsize = ImGui::CalcTextSize(buf.c_str()); dl->AddText({base->Pos.x + base->Size.x / 2 - textsize.x / 2, base->Pos.y + 2 + topmargin}, ImColor(1.0f, 1.0f, 1.0f, .5f), buf.c_str()); @@ -8252,7 +8422,7 @@ void render_prohud() dl->AddText({base->Pos.x + base->Size.x / 2 - textsize.x / 2, base->Pos.y + textsize.y + 4 + topmargin}, ImColor(1.0f, 1.0f, 1.0f, .5f), buf.c_str()); auto type = spawned_type(); - buf = fmt::format("{}", (type == "" ? "" : fmt::format("SPAWN:{}", type))); + buf = fmt::format("{}", (type == "" ? "" : fmt::format("SPAWN:{}{}", type, options["snap_to_grid"] ? " SNAP" : ""))); textsize = ImGui::CalcTextSize(buf.c_str()); dl->AddText({base->Pos.x + base->Size.x / 2 - textsize.x / 2, base->Pos.y + textsize.y * 2 + 4 + topmargin}, ImColor(1.0f, 1.0f, 1.0f, .5f), buf.c_str()); @@ -8361,6 +8531,18 @@ void imgui_init(ImGuiContext*) windows["tool_texture"] = new Window({"Texture viewer", is_tab_detached("tool_texture"), is_tab_open("tool_texture")}); // windows["tool_sound"] = new Window({"Sound player", is_tab_detached("tool_sound"), is_tab_open("tool_sound")}); + if (g_ui_scripts.find("pause") == g_ui_scripts.end()) + { + SpelunkyScript* script = new SpelunkyScript( + R"( +set_callback(function() return pause_at and pause_at ~= -1 and state.time_startup >= pause_at end, ON.PRE_UPDATE) + )", + "pause", + g_SoundManager.get(), + g_Console.get(), + true); + g_ui_scripts["pause"] = std::unique_ptr(script); + } if (g_ui_scripts.find("dark") == g_ui_scripts.end()) { SpelunkyScript* script = new SpelunkyScript( diff --git a/src/injector/main.cpp b/src/injector/main.cpp index d65dfab0d..20bf05764 100644 --- a/src/injector/main.cpp +++ b/src/injector/main.cpp @@ -360,12 +360,16 @@ bool inject_search(fs::path overlunky_path) return false; } -bool launch(fs::path exe_path, fs::path overlunky_path, bool& do_inject) +bool launch(fs::path exe_path, fs::path overlunky_path, bool& do_inject, bool& oldflip) { auto exe_dir = fs::canonical(exe_path).parent_path(); auto cwd = fs::current_path(); g_exe = exe_path.filename().string(); + std::string cmdline{"Spel2.exe"}; + if (oldflip) + cmdline = cmdline + " -oldflip"; + char dll_path[MAX_PATH] = {}; sprintf_s(dll_path, MAX_PATH, "%s", overlunky_path.string().c_str()); @@ -400,14 +404,14 @@ bool launch(fs::path exe_path, fs::path overlunky_path, bool& do_inject) STARTUPINFOA si{}; si.cb = sizeof(STARTUPINFO); - if (!do_inject && DetourCreateProcessWithDlls(NULL, (LPSTR)exe_path.string().c_str(), NULL, NULL, TRUE, CREATE_DEFAULT_ERROR_MODE, (LPVOID)child_env.c_str(), exe_dir.string().c_str(), &si, &pi, 1, dll_paths, NULL)) + if (!do_inject && DetourCreateProcessWithDlls((LPSTR)exe_path.string().c_str(), (LPSTR)cmdline.c_str(), NULL, NULL, TRUE, CREATE_DEFAULT_ERROR_MODE, (LPVOID)child_env.c_str(), exe_dir.string().c_str(), &si, &pi, 1, dll_paths, NULL)) { INFO("Game launched with DLL"); wait(); CloseHandle(pi.hThread); return true; } - else if (CreateProcess((LPSTR)exe_path.string().c_str(), NULL, NULL, NULL, TRUE, 0, (LPVOID)child_env.c_str(), exe_dir.string().c_str(), &si, &pi)) + else if (CreateProcess((LPSTR)exe_path.string().c_str(), (LPSTR)cmdline.c_str(), NULL, NULL, TRUE, 0, (LPVOID)child_env.c_str(), exe_dir.string().c_str(), &si, &pi)) { auto proc = Process{pi.hProcess, {g_exe, pi.dwProcessId}}; INFO("Game launched, injecting DLL..."); @@ -454,6 +458,7 @@ int main(int argc, char** argv) INFO("You can press ENTER to stop searching and try to launch the game from the parent folder."); INFO("Command line switches:"); INFO(" --launch_game [path] launch ../Spel2.exe, path/Spel2.exe, or a specific exe, and load OL with Detours"); + INFO(" --oldflip launch the game with -oldflip, may improve performance with external windows"); INFO(" --console keep console open to debug scripts etc"); INFO(" --inject use the old injection method instead of Detours with --launch_game"); INFO(" --info_dump output a bunch of game data to 'Spelunky 2/game_data'"); @@ -519,6 +524,7 @@ int main(int argc, char** argv) bool do_inject = GetCmdLineParam(cmd_line_parser, "inject", false); g_console = GetCmdLineParam(cmd_line_parser, "console", false); + bool oldflip = GetCmdLineParam(cmd_line_parser, "oldflip", false); if (info_dump) { do_inject = true; @@ -537,7 +543,7 @@ int main(int argc, char** argv) if (fs::exists(exe)) { - if (launch(exe, overlunky_path, do_inject)) + if (launch(exe, overlunky_path, do_inject, oldflip)) { FreeConsole(); return 0; @@ -547,7 +553,7 @@ int main(int argc, char** argv) { if (inject_search(overlunky_path)) { - launch(fs::canonical("../Spel2.exe"), overlunky_path, do_inject); + launch(fs::canonical("../Spel2.exe"), overlunky_path, do_inject, oldflip); } } FreeConsole();