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 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 @@ -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() {