improved speedrun mode

This commit is contained in:
madeline
2026-05-11 22:20:53 -07:00
parent b0f1fbee1c
commit c896bb39ea
23 changed files with 515 additions and 264 deletions
+2 -3
View File
@@ -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
+6
View File
@@ -1,6 +1,7 @@
#ifndef DUSK_CONFIG_HPP
#define DUSK_CONFIG_HPP
#include <functional>
#include <stdexcept>
#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<void(ConfigVarBase&)> callback);
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl() {
static ConfigImpl<T> config;
+46
View File
@@ -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 <typename T>
@@ -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;
}
}
};
}
+1
View File
@@ -176,6 +176,7 @@ struct UserSettings {
// Tools
ConfigVar<bool> speedrunMode;
ConfigVar<bool> liveSplitEnabled;
ConfigVar<bool> showSpeedrunRTATimer;
ConfigVar<bool> recordingMode;
} game;
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <aurora/aurora.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;
void resetForSpeedrunMode();
} // namespace dusk
+31
View File
@@ -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;
}
+1
View File
@@ -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();
+2 -1
View File
@@ -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();
}
}
+2 -1
View File
@@ -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();
}
}
+6
View File
@@ -264,3 +264,9 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
return nullptr;
}
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
for (auto& pair : RegisteredConfigVars) {
callback(*pair.second);
}
}
+1 -11
View File
@@ -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();
-3
View File
@@ -7,7 +7,6 @@
#include <aurora/aurora.h>
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuTools.hpp"
#include "dusk/main.h"
#include "imgui.h"
@@ -44,8 +43,6 @@ private:
ImVec2 m_dragScrollLastMousePos = {};
std::deque<Toast> m_toasts;
ImGuiMenuGame m_menuGame;
// Keep always last
ImGuiMenuTools m_menuTools;
-100
View File
@@ -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();
}
}
-58
View File
@@ -1,58 +0,0 @@
#ifndef DUSK_IMGUI_MENUGAME_HPP
#define DUSK_IMGUI_MENUGAME_HPP
#include <aurora/aurora.h>
#include <pad.h>
#include <string>
#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
+151 -32
View File
@@ -2,19 +2,45 @@
#include <winsock2.h>
#include <ws2tcpip.h>
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<const char*>(&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<char*>(&err), &len);
return err;
}
static constexpr int kSendFlags = 0;
#else
#include <sys/socket.h>
#include <sys/select.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); }
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 <cstdio>
@@ -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<int>(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<uint16_t>(storedPort));
if (inet_pton(AF_INET, storedHost, &addr.sin_addr) != 1) {
closeSocket(sock);
sock = INVALID_SOCKET;
return;
}
const int cr = connect(sock, reinterpret_cast<sockaddr*>(&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<uint32_t>(totalSec / 3600),
static_cast<uint32_t>((totalSec / 60) % 60),
static_cast<uint32_t>(totalSec % 60),
static_cast<uint32_t>(totalMs % 1000)
);
sendCmd(cmd);
}
+2
View File
@@ -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);
+44
View File
@@ -0,0 +1,44 @@
#include "dusk/speedrun.h"
#include "dusk/settings.h"
#include "m_Do/m_Do_main.h"
#include <aurora/aurora.h>
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
+16
View File
@@ -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<AchievementsWindow>()); });
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") &&
+74
View File
@@ -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 <SDL3/SDL_timer.h>
#include <algorithm>
#include <dolphin/pad.h>
#include <m_Do/m_Do_main.h>
#if defined(__APPLE__)
#include <TargetConditionals.h>
@@ -25,6 +29,10 @@ const Rml::String kDocumentSource = R"RML(
</head>
<body>
<fps id="fps" />
<speedrun-timer id="speedrun-timer">
<speedrun-rta id="speedrun-rta" />
<speedrun-igt id="speedrun-igt" />
</speedrun-timer>
</body>
</rml>
)RML";
@@ -204,8 +212,17 @@ void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) {
outFps = static_cast<float>(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<Window*>(top_document()) == nullptr &&
+3
View File
@@ -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;
+84 -54
View File
@@ -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<bool>& 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<float>& var,
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
std::function<bool()> 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<bool>& 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<MenuBar*>(doc.get())) {
doc = std::make_unique<MenuBar>();
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<bool>& 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.<br/><br/>"
"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.<br/><br/>Useful for "
"recording footage.",
});
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.recordingMode,
"Recording Mode",
"Disables the game HUD and all background music.<br/><br/>Useful for recording footage.");
});
}
+1
View File
@@ -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;
+1 -1
View File
@@ -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);