speedrun timer

This commit is contained in:
madeline
2026-04-12 23:22:25 -07:00
parent 460f6eea74
commit a6690c2052
13 changed files with 385 additions and 2 deletions
+4
View File
@@ -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)
+3
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
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();
}
+5
View File
@@ -79,6 +79,11 @@ struct UserSettings {
// Controls
ConfigVar<bool> enableTurboKeybind;
// Tools
ConfigVar<bool> speedrunTimer;
ConfigVar<bool> speedrunTimerOverlay;
ConfigVar<bool> liveSplitEnabled;
} game;
struct {
+4
View File
@@ -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;
}
+26 -1
View File
@@ -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;
+4
View File
@@ -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);
+18
View File
@@ -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();
}
+96
View File
@@ -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();
}
}
+13
View File
@@ -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
+183
View File
@@ -0,0 +1,183 @@
#if _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using socket_t = SOCKET;
static void closeSocket(socket_t s) { closesocket(s); }
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
using socket_t = int;
static void closeSocket(socket_t s) { close(s); }
#ifndef INVALID_SOCKET
#define INVALID_SOCKET -1
#endif
#endif
#include <cstdio>
#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
}
}
+9 -1
View File
@@ -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);
+4
View File
@@ -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;