diff --git a/CMakeLists.txt b/CMakeLists.txt index 775606ca8e..f1b4e4e26c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,10 @@ set(GAME_INCLUDE_DIRS set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient) +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 7507fb6742..cfc6fcbdea 100644 --- a/files.cmake +++ b/files.cmake @@ -1361,6 +1361,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiMenuTools.hpp src/dusk/imgui/ImGuiMenuEnhancements.cpp src/dusk/imgui/ImGuiMenuEnhancements.hpp + src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp + src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp src/dusk/imgui/ImGuiPreLaunchWindow.cpp src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiFirstRunPreset.hpp @@ -1373,6 +1375,7 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiMapLoader.cpp src/dusk/imgui/ImGuiSaveEditor.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 2ab587d3ce..1a5772e310 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -79,6 +79,11 @@ struct UserSettings { // Controls ConfigVar enableTurboKeybind; + + // Tools + ConfigVar speedrunTimer; + ConfigVar speedrunTimerOverlay; + ConfigVar liveSplitEnabled; } game; struct { diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index ac28e46838..ae4849cbe9 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -9,6 +9,7 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" +#include "dusk/livesplit.h" #include "m_Do/m_Do_controller_pad.h" dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { @@ -138,6 +139,9 @@ 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(); +#endif mCompleteCheck = true; mMode = MODE_WAIT_e; } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 1bff5da458..dc27f17c39 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -16,6 +16,7 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_main.h" #include "dusk/config.hpp" +#include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" #include "dusk/audio/DuskAudioSystem.h" @@ -38,6 +39,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(text.data(), text.data() + text.size()).x; @@ -235,6 +240,8 @@ namespace dusk { m_menuEnhancements.draw(); m_menuTools.draw(); + m_menuSpeedrunTimer.draw(); + ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80.0f * ImGuiScale()); ImGuiIO& io = ImGui::GetIO(); ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); @@ -252,12 +259,26 @@ namespace dusk { } if (dusk::IsGameLaunched && !m_isLaunchInitialized) { - m_toasts.emplace_back("Press F1 to toggle menu"s, 5.f); + DuskToast("Press F1 to toggle menu"s); m_isLaunchInitialized = true; + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } } + m_menuSpeedrunTimer.drawOverlay(); + m_menuGame.windowControllerConfig(); m_menuGame.windowInputViewer(); + + 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) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); @@ -383,6 +404,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 0296dc24cc..bec998eb63 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,6 +9,7 @@ #include "ImGuiFirstRunPreset.hpp" #include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" +#include "ImGuiMenuSpeedrunTimer.hpp" #include "ImGuiMenuTools.hpp" #include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" @@ -22,6 +23,7 @@ public: void PostDraw(); static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + void AddToast(std::string_view message, float duration = 3.f); private: struct Toast { @@ -39,6 +41,7 @@ private: ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; ImGuiMenuEnhancements m_menuEnhancements; + ImGuiMenuSpeedrunTimer m_menuSpeedrunTimer; ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last @@ -57,6 +60,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/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index 50e44f5c60..dfe8f5282b 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -2,6 +2,8 @@ #include "ImGuiMenuEnhancements.hpp" #include "ImGuiConfig.hpp" +#include "ImGuiConsole.hpp" +#include "dusk/livesplit.h" #include "dusk/settings.h" namespace dusk { @@ -181,6 +183,22 @@ namespace dusk { "This will not work with the \"Unlock Framerate\" enhancement."); } + bool prevSpeedrunTimer = getSettings().game.speedrunTimer; + config::ImGuiCheckbox("Speedrun Timer", getSettings().game.speedrunTimer); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Shows a speedrun timer in the menu bar."); + } + if ((bool)getSettings().game.speedrunTimer != prevSpeedrunTimer) { + if (!getSettings().game.speedrunTimer) { + getSettings().game.speedrunTimerOverlay.setValue(false); + if (getSettings().game.liveSplitEnabled) { + getSettings().game.liveSplitEnabled.setValue(false); + dusk::speedrun::disconnectLiveSplit(); + DuskToast("LiveSplit disconnected", 3.f); + } + } + } + ImGui::EndMenu(); } diff --git a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp new file mode 100644 index 0000000000..6edbb7e084 --- /dev/null +++ b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp @@ -0,0 +1,96 @@ +#include "fmt/format.h" +#include "imgui.h" + +#include "ImGuiMenuSpeedrunTimer.hpp" +#include "ImGuiConfig.hpp" +#include "ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/settings.h" + +namespace dusk { + static auto formatTime(uint64_t frames) { + const uint64_t totalSec = frames / 30; + + return fmt::format( + FMT_STRING("{:d}:{:02d}:{:02d}.{:02d}"), + totalSec / 3600, + (totalSec / 60) % 60, + totalSec % 60, + (int)(((f32)(frames % 30) / 30.0f) * 100.0f) + ); + } + + void ImGuiMenuSpeedrunTimer::draw() { + if (!getSettings().game.speedrunTimer) return; + + const uint64_t frames = dusk::speedrun::getFrameCount(); + + if (ImGui::BeginMenu("Timer##speedrun_timer")) { + ImGui::TextUnformatted(formatTime(frames).c_str()); + + ImGui::Separator(); + + if (ImGui::MenuItem("Reset")) { + dusk::speedrun::reset(); + } + + config::ImGuiCheckbox("Show Overlay", getSettings().game.speedrunTimerOverlay); + + bool prevLiveSplit = getSettings().game.liveSplitEnabled; + config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834."); + } + + 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 ImGuiMenuSpeedrunTimer::drawOverlay() { + if (!getSettings().game.speedrunTimer || !getSettings().game.speedrunTimerOverlay) { + return; + } + + const uint64_t frames = dusk::speedrun::getFrameCount(); + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + + const float padding = 10.f; + + ImGui::SetNextWindowPos( + ImVec2( + viewport->WorkPos.x + viewport->WorkSize.x - padding, + viewport->WorkPos.y + viewport->WorkSize.y - padding + ), + ImGuiCond_Always, ImVec2(1.f, 1.f) + ); + + ImGui::SetNextWindowBgAlpha(0.65f); + + const float fixedWidth = ImGui::CalcTextSize("9:59:59.99").x; + + ImGui::SetNextWindowContentSize(ImVec2(fixedWidth, 0.f)); + + if ( + ImGui::Begin("##speedrun_overlay", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav + ) + ) { + ImGui::TextUnformatted(formatTime(frames).c_str()); + } + ImGui::End(); + } +} diff --git a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp new file mode 100644 index 0000000000..8cb524469f --- /dev/null +++ b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp @@ -0,0 +1,13 @@ +#ifndef DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP +#define DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP + +namespace dusk { + class ImGuiMenuSpeedrunTimer { + public: + void draw(); + void drawOverlay(); + private: + }; +} + +#endif // DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP 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 0e39e55854..064bd8a86d 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -66,7 +66,12 @@ UserSettings g_userSettings = { .restoreWiiGlitches {"game.restoreWiiGlitches", false}, // Controls - .enableTurboKeybind {"game.enableTurboKeybind", false} + .enableTurboKeybind {"game.enableTurboKeybind", false}, + + // Tools + .speedrunTimer {"game.speedrunTimer", false}, + .speedrunTimerOverlay {"game.speedrunTimerOverlay", false}, + .liveSplitEnabled {"game.liveSplitEnabled", false} }, .backend = { @@ -123,6 +128,9 @@ void registerSettings() { Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); + Register(g_userSettings.game.speedrunTimer); + Register(g_userSettings.game.speedrunTimerOverlay); + Register(g_userSettings.game.liveSplitEnabled); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.enableFrameInterpolation); Register(g_userSettings.game.enableGyroAim); diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index f54ca1edad..48cf4fa16d 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" @@ -771,6 +772,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;