From b25e4d4f260735c5b61ad2971d3efd13834ccaa7 Mon Sep 17 00:00:00 2001
From: aMannus <mannusmenting@gmail.com>
Date: Wed, 31 May 2023 00:57:45 +0200
Subject: [PATCH] [Feature] In-game gameplay stats timer (#2910)

* Implement in-game gameplay stats timer

* Change timer to render on top of everything
---
 soh/include/functions.h                       |   1 +
 .../cosmetics/CosmeticsEditor.cpp             |  15 ++-
 soh/soh/Enhancements/gameplaystats.cpp        |  18 +++-
 soh/soh/Enhancements/gameplaystats.h          |   1 +
 soh/src/code/z_parameter.c                    | 102 ++++++++++++++++++
 soh/src/code/z_play.c                         |   2 +
 6 files changed, 134 insertions(+), 5 deletions(-)

diff --git a/soh/include/functions.h b/soh/include/functions.h
index 894987f8c..bb794c9df 100644
--- a/soh/include/functions.h
+++ b/soh/include/functions.h
@@ -1089,6 +1089,7 @@ void func_80088AA0(s16 seconds);
 void func_80088AF0(PlayState* play);
 void func_80088B34(s16 arg0);
 void Interface_Draw(PlayState* play);
+void Interface_DrawTotalGameplayTimer(PlayState* play);
 void Interface_Update(PlayState* play);
 Path* Path_GetByIndex(PlayState* play, s16 index, s16 max);
 f32 Path_OrientAndGetDistSq(Actor* actor, Path* path, s16 waypoint, s16* yaw);
diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp
index 0cda15d01..5b7592e80 100644
--- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp
+++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp
@@ -321,7 +321,7 @@ static std::map<std::string, CosmeticOption> cosmeticOptions = {
 static const char* MarginCvarList[] {
     "gHearts", "gHeartsCount", "gMagicBar", "gVSOA", "gBBtn", "gABtn", "gStartBtn", 
     "gCBtnU", "gCBtnD", "gCBtnL", "gCBtnR", "gDPad", "gMinimap", 
-    "gSKC", "gRC", "gCarrots",  "gTimers", "gAS", "gTCM", "gTCB"
+    "gSKC", "gRC", "gCarrots",  "gTimers", "gAS", "gTCM", "gTCB", "gIGT"
 };
 static const char* MarginCvarNonAnchor[]{ "gCarrots", "gTimers", "gAS", "gTCM","gTCB" };
 
@@ -1406,6 +1406,19 @@ void Draw_Placements(){
             ImGui::EndTable();
         }
     }
+    if (ImGui::CollapsingHeader("In-game Gameplay Timer position")) {
+        if (ImGui::BeginTable("tablegameplaytimer", 1, FlagsTable)) {
+            ImGui::TableSetupColumn("In-game Gameplay Timer settings", FlagsCell, TablesCellsWidth);
+            Table_InitHeader(false);
+            DrawUseMarginsSlider("In-game Gameplay Timer", "gIGT");
+            DrawPositionsRadioBoxes("gIGT");
+            DrawPositionSlider("gIGT", 0, ImGui::GetWindowViewport()->Size.y / 2, -50,
+                               ImGui::GetWindowViewport()->Size.x / 2 + 10);
+            DrawScaleSlider("gIGT", 1.0f);
+            ImGui::NewLine();
+            ImGui::EndTable();
+        }
+    }
 }
 
 void DrawSillyTab() {
diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp
index 4584781b4..74f0a539b 100644
--- a/soh/soh/Enhancements/gameplaystats.cpp
+++ b/soh/soh/Enhancements/gameplaystats.cpp
@@ -276,6 +276,14 @@ std::string formatHexOnlyGameplayStat(uint32_t value) {
     return fmt::format("{:#x}", value, value);
 }
 
+extern "C" char* GameplayStats_GetCurrentTime() {
+    std::string timeString = formatTimestampGameplayStat(GAMEPLAYSTAT_TOTAL_TIME).c_str();
+    const int stringLength = timeString.length();
+    char* timeChar = new char[stringLength + 1];
+    strcpy(timeChar, timeString.c_str());
+    return timeChar;
+}
+
 void LoadStatsVersion1() {
     std::string buildVersion;
     SaveManager::Instance->LoadData("buildVersion", buildVersion);
@@ -598,18 +606,20 @@ void DrawGameplayStatsBreakdownTab() {
 }
 
 void DrawGameplayStatsOptionsTab() {
-    UIWidgets::PaddedEnhancementCheckbox("Show latest timestamps on top", "gGameplayStats.TimestampsReverse");
-    UIWidgets::PaddedEnhancementCheckbox("Room Breakdown", "gGameplayStats.RoomBreakdown");
+    UIWidgets::PaddedEnhancementCheckbox("Show in-game total timer", "gGameplayStats.ShowIngameTimer", true, false);
+    UIWidgets::InsertHelpHoverText("Keep track of the timer as an in-game HUD element. The position of the timer can be changed in the Cosmetics Editor.");
+    UIWidgets::PaddedEnhancementCheckbox("Show latest timestamps on top", "gGameplayStats.TimestampsReverse", true, false);
+    UIWidgets::PaddedEnhancementCheckbox("Room Breakdown", "gGameplayStats.RoomBreakdown", true, false);
     ImGui::SameLine();
     UIWidgets::InsertHelpHoverText("Allows a more in-depth perspective of time spent in a certain map.");   
-    UIWidgets::PaddedEnhancementCheckbox("RTA Timing on new files", "gGameplayStats.RTATiming");
+    UIWidgets::PaddedEnhancementCheckbox("RTA Timing on new files", "gGameplayStats.RTATiming", true, false);
     ImGui::SameLine();
     UIWidgets::InsertHelpHoverText(
         "Timestamps are relative to starting timestamp rather than in game time, usually necessary for races/speedruns.\n\n"
         "Starting timestamp is on first non-c-up input after intro cutscene.\n\n"
         "NOTE: THIS NEEDS TO BE SET BEFORE CREATING A FILE TO TAKE EFFECT"
     );   
-    UIWidgets::PaddedEnhancementCheckbox("Show additional detail timers", "gGameplayStats.ShowAdditionalTimers");
+    UIWidgets::PaddedEnhancementCheckbox("Show additional detail timers", "gGameplayStats.ShowAdditionalTimers", true, false);
     UIWidgets::PaddedEnhancementCheckbox("Show Debug Info", "gGameplayStats.ShowDebugInfo");
 }
 
diff --git a/soh/soh/Enhancements/gameplaystats.h b/soh/soh/Enhancements/gameplaystats.h
index 49dbe2296..4cd28478b 100644
--- a/soh/soh/Enhancements/gameplaystats.h
+++ b/soh/soh/Enhancements/gameplaystats.h
@@ -17,6 +17,7 @@
     gSaveContext.sohStats.sceneTimer)
 
 void InitStatTracker();
+char* GameplayStats_GetCurrentTime();
 
 typedef enum {
     // 0x00 to 0x9B (0 to 155) used for getting items,
diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c
index 5092dc79e..83bc7fc29 100644
--- a/soh/src/code/z_parameter.c
+++ b/soh/src/code/z_parameter.c
@@ -7,6 +7,7 @@
 #include "soh/Enhancements/randomizer/adult_trade_shuffle.h"
 #include "soh/Enhancements/randomizer/randomizer_entrance.h"
 #include "libultraship/bridge.h"
+#include "soh/Enhancements/gameplaystats.h"
 
 #ifdef _MSC_VER
 #include <stdlib.h>
@@ -5979,6 +5980,107 @@ void Interface_Draw(PlayState* play) {
     CLOSE_DISPS(play->state.gfxCtx);
 }
 
+void Interface_DrawTotalGameplayTimer(PlayState* play) {
+    // Draw timer based on the Gameplay Stats total time.
+
+    if (CVarGetInteger("gGameplayStats.ShowIngameTimer", 0) && gSaveContext.fileNum >= 0 && gSaveContext.fileNum <= 2) {
+
+        s32 X_Margins_Timer = 0;
+        if (CVarGetInteger("gIGTUseMargins", 0) != 0) {
+            if (CVarGetInteger("gIGTPosType", 0) == 0) {
+                X_Margins_Timer = Left_HUD_Margin;
+            };
+        }
+        s32 rectLeftOri = OTRGetRectDimensionFromLeftEdge(24 + X_Margins_Timer);
+        s32 rectTopOri = 73;
+        if (CVarGetInteger("gIGTPosType", 0) != 0) {
+            rectTopOri = (CVarGetInteger("gIGTPosY", 0));
+            if (CVarGetInteger("gIGTPosType", 0) == 1) { // Anchor Left
+                if (CVarGetInteger("gIGTUseMargins", 0) != 0) {
+                    X_Margins_Timer = Left_HUD_Margin;
+                };
+                rectLeftOri = OTRGetRectDimensionFromLeftEdge(CVarGetInteger("gIGTPosX", 0) + X_Margins_Timer);
+            } else if (CVarGetInteger("gIGTPosType", 0) == 2) { // Anchor Right
+                if (CVarGetInteger("gIGTUseMargins", 0) != 0) {
+                    X_Margins_Timer = Right_HUD_Margin;
+                };
+                rectLeftOri = OTRGetRectDimensionFromRightEdge(CVarGetInteger("gIGTPosX", 0) + X_Margins_Timer);
+            } else if (CVarGetInteger("gIGTPosType", 0) == 3) { // Anchor None
+                rectLeftOri = CVarGetInteger("gIGTPosX", 0) + 204 + X_Margins_Timer;
+            } else if (CVarGetInteger("gIGTPosType", 0) == 4) { // Hidden
+                rectLeftOri = -9999;
+            }
+        }
+
+        s32 rectLeft;
+        s32 rectTop;
+        s32 rectWidth = 8;
+        s32 rectHeightOri = 16;
+        s32 rectHeight;
+
+        OPEN_DISPS(play->state.gfxCtx);
+
+        gDPSetCombineLERP(OVERLAY_DISP++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0,
+                          PRIMITIVE, 0);
+
+        gDPSetOtherMode(OVERLAY_DISP++,
+                        G_AD_DISABLE | G_CD_DISABLE | G_CK_NONE | G_TC_FILT | G_TF_BILERP | G_TT_IA16 | G_TL_TILE |
+                            G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE,
+                        G_AC_NONE | G_ZS_PRIM | G_RM_XLU_SURF | G_RM_XLU_SURF2);
+
+        char* totalTimeText = GameplayStats_GetCurrentTime();
+        char* textPointer = &totalTimeText[0];
+        uint8_t textLength = strlen(textPointer);
+        uint16_t textureIndex = 0;
+
+        for (uint16_t i = 0; i < textLength; i++) {
+            if (totalTimeText[i] == ':' || totalTimeText[i] == '.') {
+                textureIndex = 10;
+            } else {
+                textureIndex = totalTimeText[i] - 48;
+            }
+
+            rectLeft = rectLeftOri + (i * 8);
+            rectTop = rectTopOri;
+            rectHeight = rectHeightOri;
+
+            // Load correct digit (or : symbol)
+            gDPLoadTextureBlock(OVERLAY_DISP++, ((u8*)digitTextures[textureIndex]), G_IM_FMT_I, G_IM_SIZ_8b, rectWidth,
+                                rectHeight, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK,
+                                G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD);
+
+            // Create dot image from the colon image.
+            if (totalTimeText[i] == '.') {
+                rectHeight = rectHeight / 2;
+                rectTop += 5;
+                rectLeft -= 1;
+            }
+
+            // Draw text shadow
+            gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 0, 0, 0, 255);
+            gDPSetEnvColor(OVERLAY_DISP++, 255, 255, 255, 255);
+            gSPWideTextureRectangle(OVERLAY_DISP++, rectLeft << 2, rectTop << 2, (rectLeft + rectWidth) << 2,
+                                    (rectTop + rectHeight) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10);
+
+            // Draw regular text. Change color based on if the timer is paused, running or the game is completed.
+            if (gSaveContext.sohStats.gameComplete) {
+                gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 120, 255, 0, 255);
+            } else {
+                gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, 255);
+            }
+
+            // Offset text so underlaying shadow is to the bottom right of the text.
+            rectLeft -= 1;
+            rectTop -= 1;
+
+            gSPWideTextureRectangle(OVERLAY_DISP++, rectLeft << 2, rectTop << 2, (rectLeft + rectWidth) << 2,
+                                    (rectTop + rectHeight) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10);
+        }
+
+        CLOSE_DISPS(play->state.gfxCtx);
+    }
+}
+
 void Interface_Update(PlayState* play) {
     static u8 D_80125B60 = 0;
     static s16 sPrevTimeIncrement = 0;
diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c
index a3d275390..8c5ebb950 100644
--- a/soh/src/code/z_play.c
+++ b/soh/src/code/z_play.c
@@ -1714,6 +1714,8 @@ void Play_Draw(PlayState* play) {
     }
 
     CLOSE_DISPS(gfxCtx);
+
+    Interface_DrawTotalGameplayTimer(play);
 }
 
 time_t Play_GetRealTime() {