diff --git a/files.cmake b/files.cmake index d35d95b8a2..5b4c42ea06 100644 --- a/files.cmake +++ b/files.cmake @@ -1435,6 +1435,7 @@ set(DUSK_FILES src/dusk/layout.cpp src/dusk/logging.cpp src/dusk/settings.cpp + src/dusk/speedrun.cpp src/dusk/stubs.cpp src/dusk/update_check.cpp src/dusk/update_check.hpp @@ -1444,9 +1445,7 @@ set(DUSK_FILES src/dusk/imgui/ImGuiConsole.cpp src/dusk/imgui/ImGuiEngine.cpp src/dusk/imgui/ImGuiEngine.hpp - src/dusk/imgui/ImGuiMenuGame.cpp - src/dusk/imgui/ImGuiMenuGame.hpp - src/dusk/imgui/ImGuiBloomWindow.cpp +src/dusk/imgui/ImGuiBloomWindow.cpp src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp diff --git a/include/dusk/config.hpp b/include/dusk/config.hpp index 258e012f2b..72ec449b50 100644 --- a/include/dusk/config.hpp +++ b/include/dusk/config.hpp @@ -1,6 +1,7 @@ #ifndef DUSK_CONFIG_HPP #define DUSK_CONFIG_HPP +#include #include #include "nlohmann/json.hpp" #include "config_var.hpp" @@ -111,6 +112,11 @@ void Save(); */ ConfigVarBase* GetConfigVar(std::string_view name); +/** + * \brief Call a function on every registered CVar. + */ +void EnumerateRegistered(std::function callback); + template const ConfigImplBase* GetConfigImpl() { static ConfigImpl config; diff --git a/include/dusk/config_var.hpp b/include/dusk/config_var.hpp index a480887fb2..b16409c7f3 100644 --- a/include/dusk/config_var.hpp +++ b/include/dusk/config_var.hpp @@ -48,6 +48,13 @@ enum class ConfigVarLayer : u8 { * Will not get saved to config. */ Override, + + /** + * The CVar is temporarily overridden by speedrun mode. + * Will not get saved to config. Cleared when speedrun mode is disabled. + * Lower priority than Override, so launch args still win. + */ + Speedrun, }; class ConfigImplBase; @@ -113,6 +120,12 @@ public: * This is necessary to make it legal to access. */ void markRegistered(); + + /** + * Clear a speedrun-mode override if one is active on this CVar. + * Safe to call on any CVar, no-op if not at the Speedrun layer. + */ + virtual void clearSpeedrunOverride() {} }; template @@ -189,6 +202,7 @@ public: case ConfigVarLayer::Value: return value; case ConfigVarLayer::Override: + case ConfigVarLayer::Speedrun: return overrideValue; default: abort(); @@ -239,6 +253,38 @@ public: overrideValue = std::move(newValue); layer = ConfigVarLayer::Override; } + + /** + * \brief Give a CVar a speedrun-mode override value. + * + * Lower priority than a launch-arg override. Cleared when speedrun mode is disabled. + * The overridden value will not get saved to config. + * + * @param newValue The new value the CVar will get. + */ + void setSpeedrunValue(T newValue) { + checkRegistered(); + if (layer != ConfigVarLayer::Override) { + overrideValue = std::move(newValue); + layer = ConfigVarLayer::Speedrun; + } + } + + void clearOverride() { + checkRegistered(); + if (layer == ConfigVarLayer::Override) { + overrideValue = {}; + layer = ConfigVarLayer::Value; + } + } + + void clearSpeedrunOverride() override { + checkRegistered(); + if (layer == ConfigVarLayer::Speedrun) { + overrideValue = {}; + layer = ConfigVarLayer::Value; + } + } }; } diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 7e3c09bbb4..9f10cfad4b 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -176,6 +176,7 @@ struct UserSettings { // Tools ConfigVar speedrunMode; ConfigVar liveSplitEnabled; + ConfigVar showSpeedrunRTATimer; ConfigVar recordingMode; } game; diff --git a/include/dusk/speedrun.h b/include/dusk/speedrun.h new file mode 100644 index 0000000000..c1a92e4a50 --- /dev/null +++ b/include/dusk/speedrun.h @@ -0,0 +1,41 @@ +#pragma once +#include + +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; + +void resetForSpeedrunMode(); + +} // namespace dusk diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 3ce4d51068..20e084665b 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -201,6 +201,37 @@ fps { white-space: nowrap; } +speedrun-timer { + display: none; + position: absolute; + bottom: 0; + right: 0; + z-index: 99; + background-color: rgba(0, 0, 0, 65%); + padding: 2dp 4dp; + pointer-events: none; + font-family: "Noto Mono"; + font-size: 16dp; + color: #ffffff; + white-space: nowrap; +} + +speedrun-timer[open] { + display: block; +} + +speedrun-rta { + display: none; +} + +speedrun-rta[open] { + display: block; +} + +speedrun-igt { + display: block; +} + fps[open] { display: block; } diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index 5ac30a3cb9..55e261cc13 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -25,6 +25,7 @@ #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" +#include "dusk/speedrun.h" BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index 3ee98be5c7..42ed7845cb 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -11,6 +11,7 @@ #include "d/d_msg_string.h" #include "dusk/livesplit.h" #include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/speedrun.h" #include "m_Do/m_Do_controller_pad.h" dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { @@ -146,7 +147,7 @@ void dBrightCheck_c::modeMove() { 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::resetForSpeedrunMode(); dusk::m_speedrunInfo.startRun(); } } diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 6baa07da30..cfeeb722d6 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -11,6 +11,7 @@ #include "d/d_s_name.h" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/memory.h" +#include "dusk/speedrun.h" #include "dusk/settings.h" #include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene_mng.h" @@ -418,7 +419,7 @@ void dScnName_c::changeGameScene() { 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::resetForSpeedrunMode(); dusk::m_speedrunInfo.startRun(); } } diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 8225536d34..46a689e287 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -264,3 +264,9 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { return nullptr; } + +void dusk::config::EnumerateRegistered(std::function callback) { + for (auto& pair : RegisteredConfigVars) { + callback(*pair.second); + } +} diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index de62596308..f36deaf2c6 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -273,7 +273,6 @@ namespace dusk { // so make the window bg fully transparent temporarily ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); if (showMenu && ImGui::BeginMainMenuBar()) { - m_menuGame.draw(); m_menuTools.draw(); ImGui::EndMainMenuBar(); @@ -282,7 +281,7 @@ namespace dusk { if (dusk::IsGameLaunched && !m_isLaunchInitialized) { m_isLaunchInitialized = true; - if (getSettings().game.liveSplitEnabled) { + if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) { dusk::speedrun::connectLiveSplit(); } } @@ -353,15 +352,6 @@ namespace dusk { } m_menuTools.ShowInputViewer(); - 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(); diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 6362a146b6..c1adc427eb 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -7,7 +7,6 @@ #include -#include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" #include "dusk/main.h" #include "imgui.h" @@ -44,8 +43,6 @@ private: ImVec2 m_dragScrollLastMousePos = {}; std::deque m_toasts; - ImGuiMenuGame m_menuGame; - // Keep always last ImGuiMenuTools m_menuTools; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp deleted file mode 100644 index 783bbbdeec..0000000000 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "fmt/format.h" -#include "imgui.h" - -#include "ImGuiConsole.hpp" -#include "ImGuiConfig.hpp" - -#include "dusk/main.h" -#include "m_Do/m_Do_main.h" - -namespace dusk { - ImGuiMenuGame::ImGuiMenuGame() {} - - void ImGuiMenuGame::draw() {} - - 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); - getSettings().game.debugFlyCam.setValue(false); - getSettings().game.autoSave.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::getTrigZ(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::getTrigY(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 deleted file mode 100644 index 225a505438..0000000000 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef DUSK_IMGUI_MENUGAME_HPP -#define DUSK_IMGUI_MENUGAME_HPP - -#include -#include -#include - -#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(); - void draw(); - - void drawSpeedrunTimerOverlay(); - - static void resetForSpeedrunMode(); - - private: - bool m_showTimerWindow = false; - }; -} - -#endif // DUSK_IMGUI_MENUGAME_HPP diff --git a/src/dusk/livesplit.cpp b/src/dusk/livesplit.cpp index cec765f223..80c29379e5 100644 --- a/src/dusk/livesplit.cpp +++ b/src/dusk/livesplit.cpp @@ -2,19 +2,45 @@ #include #include using socket_t = SOCKET; - static void closeSocket(socket_t s) { closesocket(s); } + static void closeSocket(socket_t s) { + LINGER li{1, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast(&li), sizeof(li)); + closesocket(s); + } + static int socketError(socket_t s) { + int err = 0; int len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); + return err; + } + static constexpr int kSendFlags = 0; #else #include + #include #include #include #include #include #include using socket_t = int; - static void closeSocket(socket_t s) { close(s); } + static void closeSocket(socket_t s) { + struct linger li{1, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li)); + close(s); + } + static int socketError(socket_t s) { + int err = 0; socklen_t len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len); + return err; + } #ifndef INVALID_SOCKET #define INVALID_SOCKET -1 #endif + + #if defined(__APPLE__) + static constexpr int kSendFlags = 0; + #else + static constexpr int kSendFlags = MSG_NOSIGNAL; + #endif #endif #include @@ -24,12 +50,17 @@ namespace dusk::speedrun { static bool running = false; +static bool startPending = 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 uint32_t idleProbeCounter = 0; +static uint32_t reconnectCounter = 0; +static char storedHost[64] = "127.0.0.1"; +static int storedPort = 16834; static void sendCmd(const char* cmd) { if (sock == INVALID_SOCKET) { @@ -37,18 +68,20 @@ static void sendCmd(const char* cmd) { } char msg[64]; - int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd); + const int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd); + if (len <= 0 || len >= static_cast(sizeof(msg))) { + return; + } - if (send(sock, msg, len, 0) >= 0) { + if (send(sock, msg, len, kSendFlags) >= 0) { if (!connected) { connected = connectPending = true; } - return; } #if _WIN32 - int err = WSAGetLastError(); + const int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) { return; } @@ -58,10 +91,13 @@ static void sendCmd(const char* cmd) { } #endif - if (connected) disconnectPending = true; + if (connected) { + disconnectPending = true; + } closeSocket(sock); sock = INVALID_SOCKET; connected = connectPending = false; + reconnectCounter = 0; } uint64_t getFrameCount() { @@ -89,57 +125,93 @@ void start() { if (running) { return; } - + running = true; + startPending = true; frameCount = 0; wasLoading = false; - sendCmd("initgametime"); - sendCmd("reset"); - sendCmd("starttimer"); } void reset() { running = false; + startPending = false; frameCount = 0; wasLoading = false; sendCmd("reset"); } -void connectLiveSplit(const char* host, int port) { -#if _WIN32 - WSADATA wd{}; WSAStartup(MAKEWORD(2, 2), &wd); -#endif - +static void reconnect() { if (sock != INVALID_SOCKET) { - closeSocket(sock); sock = INVALID_SOCKET; + closeSocket(sock); + sock = INVALID_SOCKET; } + connected = connectPending = false; sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (sock == INVALID_SOCKET) { return; } #if _WIN32 u_long nb = 1; - ioctlsocket(sock, FIONBIO, &nb); + if (ioctlsocket(sock, FIONBIO, &nb) != 0) { + closeSocket(sock); + sock = INVALID_SOCKET; + return; + } #else - fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); + const int fl = fcntl(sock, F_GETFL, 0); + if (fl < 0 || fcntl(sock, F_SETFL, fl | O_NONBLOCK) < 0) { + closeSocket(sock); + sock = INVALID_SOCKET; + return; + } #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"); +#if defined(__APPLE__) + { + int opt = 1; + setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)); + } +#endif + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(storedPort)); + if (inet_pton(AF_INET, storedHost, &addr.sin_addr) != 1) { + closeSocket(sock); + sock = INVALID_SOCKET; + return; + } + + const int cr = connect(sock, reinterpret_cast(&addr), sizeof(addr)); +#if _WIN32 + const bool connectPending_ = cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK; +#else + const bool connectPending_ = cr < 0 && errno == EINPROGRESS; +#endif + if (cr != 0 && !connectPending_) { + closeSocket(sock); + sock = INVALID_SOCKET; + } +} + +void connectLiveSplit(const char* host, int port) { +#if _WIN32 + WSADATA wd{}; + WSAStartup(MAKEWORD(2, 2), &wd); +#endif + snprintf(storedHost, sizeof(storedHost), "%s", host); + storedPort = port; + reconnect(); } void disconnectLiveSplit() { if (sock != INVALID_SOCKET) { closeSocket(sock); sock = INVALID_SOCKET; - connected = false; } + connected = connectPending = disconnectPending = false; } bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; } @@ -147,29 +219,76 @@ bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending void updateLiveSplit() { if (sock == INVALID_SOCKET) { + if ((reconnectCounter++ % 30) == 0) { + reconnect(); + } return; } if (!connected) { + fd_set writefds, errorfds; + FD_ZERO(&writefds); + FD_ZERO(&errorfds); + FD_SET(sock, &writefds); + FD_SET(sock, &errorfds); + timeval tv{0, 0}; +#if _WIN32 + const int r = select(0, nullptr, &writefds, &errorfds, &tv); +#else + const int r = select(sock + 1, nullptr, &writefds, &errorfds, &tv); +#endif + if (r < 0 || FD_ISSET(sock, &errorfds) || socketError(sock) != 0) { + closeSocket(sock); + sock = INVALID_SOCKET; + reconnectCounter = 0; + return; + } + if (!FD_ISSET(sock, &writefds)) { + return; + } sendCmd("initgametime"); return; } + if (startPending) { + startPending = false; + sendCmd("initgametime"); + sendCmd("reset"); + sendCmd("starttimer"); + } + if (!running) { + if ((idleProbeCounter++ % 60) == 0) { + char buf; + const int r = recv(sock, &buf, 1, 0); + if (r == 0 +#if _WIN32 + || (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK) +#else + || (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK) +#endif + ) { + if (connected) { + disconnectPending = true; + } + closeSocket(sock); + sock = INVALID_SOCKET; + connected = connectPending = false; + reconnectCounter = 0; + } + } 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) + static_cast(totalSec / 3600), + static_cast((totalSec / 60) % 60), + static_cast(totalSec % 60), + static_cast(totalMs % 1000) ); - sendCmd(cmd); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 27c37b4cfc..dad122d5c1 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -113,6 +113,7 @@ UserSettings g_userSettings = { // Tools .speedrunMode {"game.speedrunMode", false}, .liveSplitEnabled {"game.liveSplitEnabled", false}, + .showSpeedrunRTATimer {"game.showSpeedrunRTATimer", true}, .recordingMode {"game.recordingMode", false} }, @@ -197,6 +198,7 @@ void registerSettings() { Register(g_userSettings.game.enableTurboKeybind); Register(g_userSettings.game.speedrunMode); Register(g_userSettings.game.liveSplitEnabled); + Register(g_userSettings.game.showSpeedrunRTATimer); Register(g_userSettings.game.recordingMode); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); diff --git a/src/dusk/speedrun.cpp b/src/dusk/speedrun.cpp new file mode 100644 index 0000000000..e2f1030b27 --- /dev/null +++ b/src/dusk/speedrun.cpp @@ -0,0 +1,44 @@ +#include "dusk/speedrun.h" +#include "dusk/settings.h" +#include "m_Do/m_Do_main.h" +#include + +namespace dusk { + +SpeedrunInfo m_speedrunInfo; + +void resetForSpeedrunMode() { + mDoMain::developmentMode = -1; + + getSettings().game.enableTurboKeybind.setSpeedrunValue(false); + + getSettings().game.damageMultiplier.setSpeedrunValue(1); + getSettings().game.instantDeath.setSpeedrunValue(false); + getSettings().game.noHeartDrops.setSpeedrunValue(false); + getSettings().game.autoSave.setSpeedrunValue(false); + getSettings().game.sunsSong.setSpeedrunValue(false); + + getSettings().game.infiniteHearts.setSpeedrunValue(false); + getSettings().game.infiniteArrows.setSpeedrunValue(false); + getSettings().game.infiniteBombs.setSpeedrunValue(false); + getSettings().game.infiniteOil.setSpeedrunValue(false); + getSettings().game.infiniteOxygen.setSpeedrunValue(false); + getSettings().game.infiniteRupees.setSpeedrunValue(false); + getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false); + getSettings().game.moonJump.setSpeedrunValue(false); + getSettings().game.superClawshot.setSpeedrunValue(false); + getSettings().game.alwaysGreatspin.setSpeedrunValue(false); + getSettings().game.enableFastIronBoots.setSpeedrunValue(false); + getSettings().game.canTransformAnywhere.setSpeedrunValue(false); + getSettings().game.fastSpinner.setSpeedrunValue(false); + getSettings().game.freeMagicArmor.setSpeedrunValue(false); + + getSettings().game.pauseOnFocusLost.setSpeedrunValue(false); + aurora_set_pause_on_focus_lost(false); + + getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false); + getSettings().game.recordingMode.setSpeedrunValue(false); + getSettings().game.debugFlyCam.setSpeedrunValue(false); +} + +} // namespace dusk diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index ea791d745b..7a8b367b9d 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -7,6 +7,8 @@ #include "achievements.hpp" #include "aurora/rmlui.hpp" +#include "dusk/speedrun.h" +#include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" #include "editor.hpp" @@ -58,6 +60,8 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( } mTabBar->add_tab("Achievements", [this] { push(std::make_unique()); }); + + mTabBar->add_tab("Reset", [this] { mTabBar->set_active_tab(-1); const auto dismiss = [](Modal& modal) { modal.pop(); }; @@ -125,6 +129,18 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( })); }); + if (getSettings().game.speedrunMode) { + mTabBar->add_tab("Reset Timer", [this] { + mTabBar->set_active_tab(-1); + mDoAud_seStartMenu(kSoundClick); + m_speedrunInfo.reset(); + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::reset(); + } + hide(false); + }); + } + // Hide document after transition completion listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 45482230a9..9a3ef78021 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -2,6 +2,9 @@ #include "aurora/lib/logging.hpp" #include "dusk/achievements.h" +#include "dusk/livesplit.h" +#include "dusk/speedrun.h" +#include "fmt/format.h" #include "magic_enum.hpp" #include "window.hpp" @@ -9,6 +12,7 @@ #include #include #include +#include #if defined(__APPLE__) #include @@ -25,6 +29,10 @@ const Rml::String kDocumentSource = R"RML( + + + + )RML"; @@ -204,8 +212,17 @@ void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) { outFps = static_cast(1.0 / avgSeconds); } +static std::string FormatTime(OSTime ticks) { + OSCalendarTime t; + OSTicksToCalendarTime(ticks, &t); + return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec); +} + Overlay::Overlay() : Document(kDocumentSource) { mFpsCounter = mDocument->GetElementById("fps"); + mSpeedrunTimer = mDocument->GetElementById("speedrun-timer"); + mSpeedrunRta = mDocument->GetElementById("speedrun-rta"); + mSpeedrunIgt = mDocument->GetElementById("speedrun-igt"); listen(mDocument, Rml::EventId::Focus, [](Rml::Event&) { Log.warn("Overlay received focus"); }); listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) { @@ -268,6 +285,63 @@ void Overlay::update() { } } +#if !(defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST)) + if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) { + dusk::speedrun::updateLiveSplit(); + if (dusk::speedrun::consumeConnectedEvent()) { + push_toast({.title = "LiveSplit connected", .duration = std::chrono::seconds(3)}); + } + if (dusk::speedrun::consumeDisconnectedEvent()) { + push_toast({.title = "LiveSplit disconnected", .duration = std::chrono::seconds(3)}); + } + } +#endif + + if (mSpeedrunTimer != nullptr && mSpeedrunRta != nullptr && mSpeedrunIgt != nullptr) { + if (getSettings().game.speedrunMode) { + // 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::getTrigZ(PAD_1)) + { + m_speedrunInfo.reset(); + } + + // L+R+A+Y to manually stop timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && + mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) + { + if (m_speedrunInfo.m_isRunStarted) { + m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; + m_speedrunInfo.m_isRunStarted = false; + } + } + + 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; + } + + if (!m_speedrunInfo.m_isPauseIGT) { + m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime; + } + + mSpeedrunTimer->SetAttribute("open", ""); + + if (getSettings().game.showSpeedrunRTATimer) { + mSpeedrunRta->SetAttribute("open", ""); + mSpeedrunRta->SetInnerRML(escape(fmt::format("RTA {}", FormatTime(elapsedTime)))); + } else { + mSpeedrunRta->RemoveAttribute("open"); + } + + mSpeedrunIgt->SetInnerRML(escape(fmt::format("IGT {}", FormatTime(m_speedrunInfo.m_igtTimer)))); + } else { + mSpeedrunTimer->RemoveAttribute("open"); + } + } + const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 && PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr && dynamic_cast(top_document()) == nullptr && diff --git a/src/dusk/ui/overlay.hpp b/src/dusk/ui/overlay.hpp index 5c1433c51b..8a2edd4a26 100644 --- a/src/dusk/ui/overlay.hpp +++ b/src/dusk/ui/overlay.hpp @@ -21,6 +21,9 @@ protected: Rml::Element* mCurrentToast = nullptr; Rml::Element* mControllerWarning = nullptr; Rml::Element* mMenuNotification = nullptr; + Rml::Element* mSpeedrunTimer = nullptr; + Rml::Element* mSpeedrunRta = nullptr; + Rml::Element* mSpeedrunIgt = nullptr; clock::time_point mCurrentToastStartTime; clock::time_point mMenuNotificationStartTime; diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 83f6cceac5..93c1053362 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -166,29 +166,46 @@ AuroraBackend configured_backend() { void reset_for_speedrun_mode() { mDoMain::developmentMode = -1; - getSettings().game.damageMultiplier.setValue(1); - getSettings().game.instantDeath.setValue(false); - getSettings().game.noHeartDrops.setValue(false); + getSettings().game.enableTurboKeybind.setSpeedrunValue(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.damageMultiplier.setSpeedrunValue(1); + getSettings().game.instantDeath.setSpeedrunValue(false); + getSettings().game.noHeartDrops.setSpeedrunValue(false); + getSettings().game.autoSave.setSpeedrunValue(false); + getSettings().game.sunsSong.setSpeedrunValue(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.infiniteHearts.setSpeedrunValue(false); + getSettings().game.infiniteArrows.setSpeedrunValue(false); + getSettings().game.infiniteBombs.setSpeedrunValue(false); + getSettings().game.infiniteOil.setSpeedrunValue(false); + getSettings().game.infiniteOxygen.setSpeedrunValue(false); + getSettings().game.infiniteRupees.setSpeedrunValue(false); + getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false); + getSettings().game.moonJump.setSpeedrunValue(false); + getSettings().game.superClawshot.setSpeedrunValue(false); + getSettings().game.alwaysGreatspin.setSpeedrunValue(false); + getSettings().game.enableFastIronBoots.setSpeedrunValue(false); + getSettings().game.canTransformAnywhere.setSpeedrunValue(false); + getSettings().game.fastSpinner.setSpeedrunValue(false); + getSettings().game.freeMagicArmor.setSpeedrunValue(false); - getSettings().game.enableTurboKeybind.setValue(false); - getSettings().game.debugFlyCam.setValue(false); - getSettings().game.autoSave.setValue(false); + getSettings().game.pauseOnFocusLost.setSpeedrunValue(false); + aurora_set_pause_on_focus_lost(false); + + getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false); + getSettings().game.recordingMode.setSpeedrunValue(false); + getSettings().game.debugFlyCam.setSpeedrunValue(false); +} + +void clear_speedrun_overrides() { + config::EnumerateRegistered([](config::ConfigVarBase& cvar) { + cvar.clearSpeedrunOverride(); + }); +} + +void restore_from_speedrun_mode() { + clear_speedrun_overrides(); + aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue()); } const Rml::String kInternalResolutionHelpText = @@ -252,6 +269,15 @@ SelectButton& config_bool_select( return button; } +void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar& var, + const Rml::String& key, const Rml::String& helpText) { + config_bool_select(leftPane, rightPane, var, { + .key = key, + .helpText = helpText, + .isDisabled = [] { return getSettings().game.speedrunMode; }, + }); +} + SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar& var, Rml::String key, Rml::String helpText, int min, int max, int step = 5, std::function isDisabled = {}) { @@ -492,7 +518,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost, { .key = "Pause on Focus Lost", - .isDisabled = [] { return IsMobile; }, + .helpText = "Pause the game when window focus is lost.", + .onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); }, + .isDisabled = [] { return IsMobile || getSettings().game.speedrunMode; }, }); leftPane.register_control( leftPane.add_select_button({ @@ -801,12 +829,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }; auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText) { - config_bool_select(leftPane, rightPane, value, - { - .key = key, - .helpText = helpText, - .isDisabled = [] { return getSettings().game.speedrunMode; }, - }); + add_speedrun_disabled_option(leftPane, rightPane, value, key, helpText); }; leftPane.add_section("General"); @@ -858,12 +881,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { "Quicker climbing on ladders and vines like the HD version."); addOption("Faster Tears of Light", getSettings().game.fastTears, "Tears of Light dropped by Shadow Insects pop out faster like the HD version."); - config_bool_select(leftPane, rightPane, getSettings().game.autoSave, - { - .key = "Autosave", - .helpText = "Autosaves the game when going to a new area, opening a dungeon door, " - "or getting a new item.", - }); + addSpeedrunDisabledOption("Autosave", getSettings().game.autoSave, + "Autosaves the game when going to a new area, opening a dungeon door, " + "or getting a new item."); addOption("Instant Saves", getSettings().game.instantSaves, "Skips the delay when writing to the Memory Card."); addOption("Hold B for Instant Text", getSettings().game.instantText, @@ -877,7 +897,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { "Link will not recoil when his sword hits walls."); addOption("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat, "Skip needing to catch a second fish for Sera's cat."); - addOption("Sun's Song (R+X)", getSettings().game.sunsSong, + addSpeedrunDisabledOption("Sun's Song (R+X)", getSettings().game.sunsSong, "Allows Wolf Link to howl and change the time of day."); addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform, "Transform instantly by pressing R and Y simultaneously."); @@ -888,12 +908,29 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .key = "Speedrun Mode", .helpText = "Enables speedrunning options while restricting certain gameplay modifiers.", - .onChange = [](bool) { reset_for_speedrun_mode(); }, + .onChange = + [](bool enabled) { + if (enabled) { + reset_for_speedrun_mode(); + } else { + restore_from_speedrun_mode(); + if (getSettings().game.liveSplitEnabled) { + speedrun::disconnectLiveSplit(); + } + } + for (auto& doc : get_document_stack()) { + if (dynamic_cast(doc.get())) { + doc = std::make_unique(); + break; + } + } + }, }); config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled, { .key = "LiveSplit Connection", - .helpText = "Connect to LiveSplit server on localhost:16834.", + .helpText = "Connect to LiveSplit server on localhost:16834. For this to work you must right click LiveSplit, and turn on Control -> Start TCP Server." + " To see IGT in LiveSplit you must change your comparison to Game Time.", .onChange = [](bool enabled) { if (enabled) { @@ -902,6 +939,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { speedrun::disconnectLiveSplit(); } }, + .isDisabled = [] { return IsMobile || !getSettings().game.speedrunMode; }, + }); + config_bool_select(leftPane, rightPane, getSettings().game.showSpeedrunRTATimer, + { + .key = "Show RTA", + .helpText = "Display the RTA timer. IGT is always visible.", .isDisabled = [] { return !getSettings().game.speedrunMode; }, }); }); @@ -912,12 +955,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { auto addCheat = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText) { - config_bool_select(leftPane, rightPane, value, - { - .key = key, - .helpText = helpText, - .isDisabled = [] { return getSettings().game.speedrunMode; }, - }); + add_speedrun_disabled_option(leftPane, rightPane, value, key, helpText); }; leftPane.add_section("Resources"); @@ -1068,12 +1106,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .helpText = "Checks GitHub releases for a new Dusk version on startup.

" "No personal information is transmitted or collected.", }); - config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost, - { - .key = "Pause On Focus Lost", - .helpText = "Pause the game when window focus is lost.", - .onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); }, - }); config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings, { .key = "Enable Advanced Settings", @@ -1090,6 +1122,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { } } }, + .isDisabled = [] { return getSettings().game.speedrunMode; }, }); leftPane.add_section("Game"); @@ -1098,12 +1131,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .key = "Skip TV Settings Screen", .helpText = "Skips the TV calibration screen shown when loading a save.", }); - config_bool_select(leftPane, rightPane, getSettings().game.recordingMode, - { - .key = "Recording Mode", - .helpText = "Disables the game HUD and all background music.

Useful for " - "recording footage.", - }); + add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.recordingMode, + "Recording Mode", + "Disables the game HUD and all background music.

Useful for recording footage."); }); } diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index 5ef15a6c41..fc2d5d6691 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -53,6 +53,7 @@ bool initialize() noexcept { load_font("AlegreyaSC-Regular.ttf"); load_font("AlegreyaSC-Bold.ttf"); load_font("MaterialSymbolsRounded-Regular.ttf"); + load_font("NotoMono-Regular.ttf"); sInitialized = true; return true; diff --git a/src/f_op/f_op_overlap_req.cpp b/src/f_op/f_op_overlap_req.cpp index 4d410fb2c2..37b901c706 100644 --- a/src/f_op/f_op_overlap_req.cpp +++ b/src/f_op/f_op_overlap_req.cpp @@ -7,7 +7,7 @@ #include "f_op/f_op_overlap_req.h" #include "f_pc/f_pc_manager.h" -#include "dusk/imgui/ImGuiMenuGame.hpp" +#include "dusk/speedrun.h" void fopOvlpReq_SetPeektime(overlap_request_class*, u16);