diff --git a/CMakeLists.txt b/CMakeLists.txt index b326931e79..46f0b1a411 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -306,6 +306,10 @@ if (DUSK_ENABLE_SENTRY_NATIVE) list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1) endif () +if (WIN32) + list(APPEND GAME_LIBS Ws2_32) +endif () + if (DUSK_MOVIE_SUPPORT) if (TARGET libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) diff --git a/files.cmake b/files.cmake index f001a72f47..f91756a9d5 100644 --- a/files.cmake +++ b/files.cmake @@ -1464,6 +1464,7 @@ set(DUSK_FILES src/dusk/imgui/ImGuiAchievements.cpp src/dusk/achievements.cpp src/dusk/iso_validate.cpp + src/dusk/livesplit.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp src/dusk/OSThread.cpp diff --git a/include/dusk/livesplit.h b/include/dusk/livesplit.h new file mode 100644 index 0000000000..b283a29af4 --- /dev/null +++ b/include/dusk/livesplit.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace dusk::speedrun { +void onGameFrame(); +uint64_t getFrameCount(); +void start(); +void reset(); +void connectLiveSplit(const char* host = "127.0.0.1", int port = 16834); +void disconnectLiveSplit(); +bool consumeConnectedEvent(); +bool consumeDisconnectedEvent(); +void updateLiveSplit(); +void shutdown(); +} diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 875b839d44..2acc69e43c 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -138,6 +138,10 @@ struct UserSettings { // Controls ConfigVar enableTurboKeybind; + + // Tools + ConfigVar speedrunMode; + ConfigVar liveSplitEnabled; } game; struct { diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index aa77b24129..5ac30a3cb9 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,6 +23,7 @@ #include "d/actor/d_a_npc_tkc.h" #include +#include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" BOOL daAlink_c::checkEventRun() const { @@ -4005,6 +4006,15 @@ int daAlink_c::procGanonFinishInit() { field_0x37c8 = current.pos; onEndResetFlg1(ERFLG1_SHIELD_BACKBONE); + +#if TARGET_PC + if (dusk::getSettings().game.speedrunMode) { + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.stopRun(); + } + } +#endif + return 1; } diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index c3fcc806d5..3ee98be5c7 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -9,6 +9,8 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" +#include "dusk/livesplit.h" +#include "dusk/imgui/ImGuiConsole.hpp" #include "m_Do/m_Do_controller_pad.h" dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { @@ -138,6 +140,17 @@ void dBrightCheck_c::modeWait() {} void dBrightCheck_c::modeMove() { if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); +#ifdef TARGET_PC + dusk::speedrun::start(); + + if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { + // start a new run if a run isn't already in progress + if (!dusk::m_speedrunInfo.m_isRunStarted) { + dusk::ImGuiMenuGame::resetForSpeedrunMode(); + dusk::m_speedrunInfo.startRun(); + } + } +#endif mCompleteCheck = true; mMode = MODE_WAIT_e; } diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 735ce4d0ca..6baa07da30 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -5,19 +5,20 @@ #include "d/dolzel.h" // IWYU pragma: keep -#include "d/d_s_name.h" #include "JSystem/JKernel/JKRExpHeap.h" #include "d/d_com_inf_game.h" #include "d/d_meter2_info.h" +#include "d/d_s_name.h" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/memory.h" +#include "dusk/settings.h" +#include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" #include "m_Do/m_Do_graphic.h" #include "m_Do/m_Do_machine.h" -#include "m_Do/m_Do_mtx.h" #include "m_Do/m_Do_main.h" -#include "f_op/f_op_overlap_mng.h" -#include "dusk/memory.h" -#include "dusk/settings.h" +#include "m_Do/m_Do_mtx.h" #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -412,6 +413,16 @@ void dScnName_c::changeGameScene() { dKy_clear_game_init(); dComIfGs_resetDan(); dComIfGs_setRestartRoomParam(0); + +#if TARGET_PC + if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) { + // start a new run on file load if a run isn't already in progress + if (!dusk::m_speedrunInfo.m_isRunStarted) { + dusk::ImGuiMenuGame::resetForSpeedrunMode(); + dusk::m_speedrunInfo.startRun(); + } + } +#endif } } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index c8a9fa86aa..6bca83482e 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -19,6 +19,7 @@ #include "dusk/config.hpp" #include "dusk/dusk.h" #include "dusk/frame_interpolation.h" +#include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" @@ -65,6 +66,10 @@ namespace dusk { ImGui::TextUnformatted(text.data(), text.data() + text.size()); } + void DuskToast(std::string_view message, float duration) { + g_imguiConsole.AddToast(message, duration); + } + void ImGuiTextCenter(std::string_view text) { ImGui::NewLine(); float fontSize = ImGui::CalcTextSize( @@ -365,13 +370,26 @@ namespace dusk { "Press F1 to toggle menu"s, 2.5f); m_isLaunchInitialized = true; + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } } UpdateDragScroll(); m_menuGame.windowControllerConfig(); m_menuGame.windowInputViewer(); - if (dusk::IsGameLaunched) { + m_menuGame.drawSpeedrunTimerOverlay(); + + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::updateLiveSplit(); + if (dusk::speedrun::consumeConnectedEvent()) + AddToast("LiveSplit connected"); + else if (dusk::speedrun::consumeDisconnectedEvent()) + AddToast("LiveSplit disconnected"); + } + + if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); m_menuTools.ShowProcessManager(); @@ -382,8 +400,8 @@ namespace dusk { m_menuTools.ShowPlayerInfo(); m_menuTools.ShowAudioDebug(); m_menuTools.ShowSaveEditor(); + m_menuTools.ShowStateShare(); } - m_menuTools.ShowStateShare(); m_menuTools.ShowAchievements(); DuskDebugPad(); // temporary, remove later @@ -556,6 +574,10 @@ namespace dusk { return false; } + void ImGuiConsole::AddToast(std::string_view message, float duration) { + m_toasts.emplace_back(std::string(message), duration); + } + void ImGuiConsole::ShowToasts() { if (m_toasts.empty()) { return; diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index de4e66c7d6..a6c8b48fc7 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -27,6 +27,7 @@ public: void PostDraw(); static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + void AddToast(std::string_view message, float duration = 3.f); private: struct Toast { @@ -70,6 +71,7 @@ std::string BytesToString(size_t bytes); void SetOverlayWindowLocation(int corner); bool ShowCornerContextMenu(int& corner, int avoidCorner); void ImGuiStringViewText(std::string_view text); +void DuskToast(std::string_view message, float duration = 3.f); void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); void ImGuiEndGroupPanel(); void ImGuiTextCenter(std::string_view text); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 10b6e65bf2..d0da643b0c 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -12,12 +12,15 @@ #include "dusk/main.h" #include "dusk/hotkeys.h" #include "dusk/settings.h" +#include "dusk/livesplit.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include #include +#include "m_Do/m_Do_main.h" + namespace { constexpr int kInternalResolutionScaleMax = 12; } // namespace @@ -201,6 +204,7 @@ namespace dusk { ImGui::SeparatorText("Difficulty"); + ImGui::BeginDisabled(getSettings().game.speedrunMode); config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); @@ -213,6 +217,7 @@ namespace dusk { ImGui::SetTooltip("Hearts will never drop from enemies,\n" "pots and various other places."); } + ImGui::EndDisabled(); ImGui::SeparatorText("Quality of Life"); @@ -282,12 +287,39 @@ namespace dusk { ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); } + ImGui::SeparatorText("Speedrunning"); + if (config::ImGuiCheckbox("Speedrun Mode", getSettings().game.speedrunMode)) { + resetForSpeedrunMode(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables Speedrunning options, while restricting certain gameplay modifiers."); + } + + ImGui::BeginDisabled(!getSettings().game.speedrunMode); + bool prevLiveSplit = getSettings().game.liveSplitEnabled; + config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834."); + } + ImGui::EndDisabled(); + + if ((bool)getSettings().game.liveSplitEnabled != prevLiveSplit) { + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } else { + dusk::speedrun::disconnectLiveSplit(); + DuskToast("LiveSplit disconnected", 3.f); + } + } + ImGui::EndMenu(); } } void ImGuiMenuGame::drawCheatsMenu() { if (ImGui::BeginMenu("Cheats")) { + ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::SeparatorText("Resources"); config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); @@ -295,8 +327,8 @@ namespace dusk { config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); - config::ImGuiCheckbox("Items Don't Despawn", getSettings().game.enableIndefiniteItemDrops); - ImGui::SetItemTooltip("Items Don't Despawn Unless You Load A Different Room In Which Case They Do But Even Under Some Circumstances They Don't, It Is Quite Rare Though"); + config::ImGuiCheckbox("No Item Timer", getSettings().game.enableIndefiniteItemDrops); + ImGui::SetItemTooltip("Item drops such as Rupees, Hearts, etc. will never disappear after they drop."); ImGui::SeparatorText("Abilities"); config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); @@ -319,6 +351,8 @@ namespace dusk { ImGui::SetTooltip("Makes the magic armor work without rupees."); } + ImGui::EndDisabled(); + ImGui::EndMenu(); } } @@ -441,10 +475,12 @@ namespace dusk { ImGui::SeparatorText("Tools"); + ImGui::BeginDisabled(getSettings().game.speedrunMode); config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); } + ImGui::EndDisabled(); ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); @@ -1003,4 +1039,88 @@ namespace dusk { ImGui::End(); } + + static std::string GetFormattedTime(OSTime ticks) { + OSCalendarTime time; + OSTicksToCalendarTime(ticks, &time); + + return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", time.hour, time.min, time.sec, time.msec); + } + + void ImGuiMenuGame::resetForSpeedrunMode() { + // reset settings that should be off for speedrun mode + mDoMain::developmentMode = -1; + + getSettings().game.damageMultiplier.setValue(1); + getSettings().game.instantDeath.setValue(false); + getSettings().game.noHeartDrops.setValue(false); + + getSettings().game.infiniteHearts.setValue(false); + getSettings().game.infiniteArrows.setValue(false); + getSettings().game.infiniteBombs.setValue(false); + getSettings().game.infiniteOil.setValue(false); + getSettings().game.infiniteOxygen.setValue(false); + getSettings().game.infiniteRupees.setValue(false); + getSettings().game.enableIndefiniteItemDrops.setValue(false); + + getSettings().game.moonJump.setValue(false); + getSettings().game.superClawshot.setValue(false); + getSettings().game.alwaysGreatspin.setValue(false); + getSettings().game.enableFastIronBoots.setValue(false); + getSettings().game.canTransformAnywhere.setValue(false); + getSettings().game.fastSpinner.setValue(false); + getSettings().game.freeMagicArmor.setValue(false); + + getSettings().game.enableTurboKeybind.setValue(false); + } + + SpeedrunInfo m_speedrunInfo; + + void ImGuiMenuGame::drawSpeedrunTimerOverlay() { + if (!getSettings().game.speedrunMode) { + return; + } + + // L+R+A+Start to reset timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigStart(PAD_1)) { + m_speedrunInfo.reset(); + } + + // L+R+A+Z to manually stop timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { + if (m_speedrunInfo.m_isRunStarted) { + m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; + m_speedrunInfo.m_isRunStarted = false; + } + } + + ImGui::SetNextWindowBgAlpha(0.65f); + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoDocking + | ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoScrollbar; + + if (ImGui::Begin("##SpeedrunTimerWindow", nullptr, flags)) { + OSTime elapsedTime = 0; + if (m_speedrunInfo.m_isRunStarted) { + elapsedTime = OSGetTime() - m_speedrunInfo.m_startTimestamp; + } else if (m_speedrunInfo.m_endTimestamp != 0) { + elapsedTime = m_speedrunInfo.m_endTimestamp; + } + + ImGui::Text("RTA"); + ImGui::SameLine(60.0f); + ImGuiStringViewText(GetFormattedTime(elapsedTime)); + + if (!m_speedrunInfo.m_isPauseIGT) { + m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime; + } + + ImGui::Text("IGT"); + ImGui::SameLine(60.0f); + ImGuiStringViewText(GetFormattedTime(m_speedrunInfo.m_igtTimer)); + } + ImGui::End(); + } } diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index fb3f3f10ca..e54541a010 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -8,6 +8,39 @@ #include "imgui.h" namespace dusk { + struct SpeedrunInfo { + void startRun() { + m_isRunStarted = true; + m_startTimestamp = OSGetTime(); + } + + void stopRun() { + m_isRunStarted = false; + m_endTimestamp = OSGetTime() - m_startTimestamp; + } + + void reset() { + m_isRunStarted = false; + m_startTimestamp = 0; + m_endTimestamp = 0; + m_isPauseIGT = false; + m_loadStartTimestamp = 0; + m_totalLoadTime = 0; + m_igtTimer = 0; + } + + bool m_isRunStarted = false; + OSTime m_startTimestamp = 0; + OSTime m_endTimestamp = 0; + + bool m_isPauseIGT = false; + OSTime m_loadStartTimestamp = 0; + OSTime m_totalLoadTime = 0; + OSTime m_igtTimer = 0; + }; + + extern SpeedrunInfo m_speedrunInfo; + class ImGuiMenuGame { public: ImGuiMenuGame(); @@ -15,9 +48,12 @@ namespace dusk { void windowInputViewer(); void windowControllerConfig(); + void drawSpeedrunTimerOverlay(); static void ToggleFullscreen(); + static void resetForSpeedrunMode(); + private: void drawAudioMenu(); void drawInputMenu(); @@ -41,6 +77,8 @@ namespace dusk { bool m_showInputViewerGyro = false; int m_inputOverlayCorner = 3; std::string m_controllerName; + + bool m_showTimerWindow = false; }; } diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index a82067a438..2e3738aab9 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -50,10 +50,14 @@ namespace dusk { ImGui::BeginDisabled(); } + ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); + ImGui::EndDisabled(); + if (!dusk::IsGameLaunched) { ImGui::EndDisabled(); } @@ -71,6 +75,8 @@ namespace dusk { } if (ImGui::BeginMenu("Debug")) { + ImGui::BeginDisabled(getSettings().game.speedrunMode); + bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { mDoMain::developmentMode = developmentMode ? 1 : -1; @@ -119,6 +125,9 @@ namespace dusk { } ImGui::MenuItem("OSReport Force", nullptr, &OSReportReallyForceEnable); + + ImGui::EndDisabled(); + ImGui::EndMenu(); } } diff --git a/src/dusk/livesplit.cpp b/src/dusk/livesplit.cpp new file mode 100644 index 0000000000..cec765f223 --- /dev/null +++ b/src/dusk/livesplit.cpp @@ -0,0 +1,183 @@ +#if _WIN32 + #include + #include + using socket_t = SOCKET; + static void closeSocket(socket_t s) { closesocket(s); } +#else + #include + #include + #include + #include + #include + #include + using socket_t = int; + static void closeSocket(socket_t s) { close(s); } + #ifndef INVALID_SOCKET + #define INVALID_SOCKET -1 + #endif +#endif + +#include +#include "dusk/livesplit.h" +#include "f_op/f_op_overlap_mng.h" + +namespace dusk::speedrun { + +static bool running = false; +static uint64_t frameCount = 0; +static socket_t sock = INVALID_SOCKET; +static bool wasLoading = false; +static bool connected = false; +static bool connectPending = false; +static bool disconnectPending = false; + +static void sendCmd(const char* cmd) { + if (sock == INVALID_SOCKET) { + return; + } + + char msg[64]; + int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd); + + if (send(sock, msg, len, 0) >= 0) { + if (!connected) { + connected = connectPending = true; + } + + return; + } + +#if _WIN32 + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) { + return; + } +#else + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOTCONN) { + return; + } +#endif + + if (connected) disconnectPending = true; + closeSocket(sock); + sock = INVALID_SOCKET; + connected = connectPending = false; +} + +uint64_t getFrameCount() { + return frameCount; +} + +void onGameFrame() { + if (!running) { + return; + } + + bool loading = fopOvlpM_IsDoingReq() != 0; + + if (loading != wasLoading) { + sendCmd(loading ? "pausegametime" : "unpausegametime"); + wasLoading = loading; + } + + if (!loading) { + ++frameCount; + } +} + +void start() { + if (running) { + return; + } + + running = true; + frameCount = 0; + wasLoading = false; + sendCmd("initgametime"); + sendCmd("reset"); + sendCmd("starttimer"); +} + +void reset() { + running = false; + frameCount = 0; + wasLoading = false; + sendCmd("reset"); +} + +void connectLiveSplit(const char* host, int port) { +#if _WIN32 + WSADATA wd{}; WSAStartup(MAKEWORD(2, 2), &wd); +#endif + + if (sock != INVALID_SOCKET) { + closeSocket(sock); sock = INVALID_SOCKET; + } + + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (sock == INVALID_SOCKET) { + return; + } + +#if _WIN32 + u_long nb = 1; + ioctlsocket(sock, FIONBIO, &nb); +#else + fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); +#endif + + sockaddr_in addr{}; addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + inet_pton(AF_INET, host, &addr.sin_addr); + connect(sock, (sockaddr*)&addr, sizeof(addr)); + sendCmd("initgametime"); +} + +void disconnectLiveSplit() { + if (sock != INVALID_SOCKET) { + closeSocket(sock); + sock = INVALID_SOCKET; + connected = false; + } +} + +bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; } +bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending = false; return v; } + +void updateLiveSplit() { + if (sock == INVALID_SOCKET) { + return; + } + + if (!connected) { + sendCmd("initgametime"); + return; + } + + if (!running) { + return; + } + + const uint64_t totalMs = frameCount * 1000 / 30; + const uint64_t totalSec = totalMs / 1000; + char cmd[32]; + + snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u", + (uint32_t)(totalSec / 3600), + (uint32_t)((totalSec / 60) % 60), + (uint32_t)(totalSec % 60), + (uint32_t)(totalMs % 1000) + ); + + sendCmd(cmd); +} + +void shutdown() { + disconnectLiveSplit(); +#if _WIN32 + WSACleanup(); +#endif +} + +} diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 84e56eda4f..ee6ba663ca 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -96,7 +96,11 @@ UserSettings g_userSettings = { .restoreWiiGlitches {"game.restoreWiiGlitches", false}, // Controls - .enableTurboKeybind {"game.enableTurboKeybind", false} + .enableTurboKeybind {"game.enableTurboKeybind", false}, + + // Tools + .speedrunMode {"game.speedrunMode", false}, + .liveSplitEnabled {"game.liveSplitEnabled", false} }, .backend = { @@ -169,6 +173,8 @@ void registerSettings() { Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); + Register(g_userSettings.game.speedrunMode); + Register(g_userSettings.game.liveSplitEnabled); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); Register(g_userSettings.game.infiniteArrows); diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 78fc240db4..beec5847ef 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -15,6 +15,7 @@ #include "d/d_model.h" #include "d/d_tresure.h" #include "dusk/frame_interpolation.h" +#include "dusk/livesplit.h" #include "dusk/logging.h" #include "f_op/f_op_camera_mng.h" #include "f_op/f_op_draw_tag.h" @@ -815,6 +816,9 @@ void fapGm_Execute() { fpcM_ManagementFunc(NULL, fapGm_After); #endif cCt_Counter(0); +#ifdef TARGET_PC + dusk::speedrun::onGameFrame(); +#endif } fapGm_HIO_c g_HIO; diff --git a/src/f_op/f_op_overlap_req.cpp b/src/f_op/f_op_overlap_req.cpp index 28d0a743cc..4d410fb2c2 100644 --- a/src/f_op/f_op_overlap_req.cpp +++ b/src/f_op/f_op_overlap_req.cpp @@ -7,6 +7,8 @@ #include "f_op/f_op_overlap_req.h" #include "f_pc/f_pc_manager.h" +#include "dusk/imgui/ImGuiMenuGame.hpp" + void fopOvlpReq_SetPeektime(overlap_request_class*, u16); static int fopOvlpReq_phase_Done(overlap_request_class* i_overlapReq) { @@ -16,6 +18,16 @@ static int fopOvlpReq_phase_Done(overlap_request_class* i_overlapReq) { i_overlapReq->peektime = 0; i_overlapReq->field_0x8 = 0; i_overlapReq->field_0xc = 0; + + #if TARGET_PC + if (dusk::getSettings().game.speedrunMode) { + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.m_isPauseIGT = false; + dusk::m_speedrunInfo.m_totalLoadTime += OSGetTime() - dusk::m_speedrunInfo.m_loadStartTimestamp; + dusk::m_speedrunInfo.m_loadStartTimestamp = OSGetTime(); + } + } + #endif return cPhs_NEXT_e; } @@ -81,6 +93,14 @@ static int fopOvlpReq_phase_Create(overlap_request_class* i_overlapReq) { fpcLy_SetCurrentLayer(i_overlapReq->layer); i_overlapReq->request_id = fpcM_Create(i_overlapReq->procname, NULL, NULL); + +#if TARGET_PC + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.m_isPauseIGT = true; + dusk::m_speedrunInfo.m_loadStartTimestamp = OSGetTime(); + } +#endif + return cPhs_NEXT_e; } diff --git a/src/f_op/f_op_scene_req.cpp b/src/f_op/f_op_scene_req.cpp index 79b87eee4c..abcb6ce848 100644 --- a/src/f_op/f_op_scene_req.cpp +++ b/src/f_op/f_op_scene_req.cpp @@ -4,13 +4,14 @@ */ #include "f_op/f_op_scene_req.h" +#include +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/logging.h" #include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene.h" #include "f_op/f_op_scene_pause.h" #include "f_pc/f_pc_executor.h" #include "f_pc/f_pc_manager.h" -#include -#include "dusk/logging.h" static cPhs_Step fopScnRq_phase_ClearOverlap(scene_request_class* i_sceneReq) { return fopOvlpM_ClearOfReq() == 1 ? cPhs_NEXT_e : cPhs_INIT_e;