Cleanup gameplay stats code/UI and support RTA timing (#2862)

This commit is contained in:
Garrett Cox 2023-05-21 22:35:56 +00:00 committed by GitHub
parent f2f5a75cb0
commit 5de1240391
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 470 additions and 406 deletions

View File

@ -83,6 +83,8 @@ typedef struct {
/* */ u32 entrancesDiscovered[SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT]; /* */ u32 entrancesDiscovered[SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT];
/* */ u32 scenesDiscovered[SAVEFILE_SCENES_DISCOVERED_IDX_COUNT]; /* */ u32 scenesDiscovered[SAVEFILE_SCENES_DISCOVERED_IDX_COUNT];
/* */ u8 locationsSkipped[RC_MAX]; /* */ u8 locationsSkipped[RC_MAX];
/* */ bool rtaTiming;
/* */ uint64_t fileCreatedAt;
} SohStats; } SohStats;
typedef struct { typedef struct {

View File

@ -14,120 +14,215 @@ extern "C" {
#include <z64.h> #include <z64.h>
#include "variables.h" #include "variables.h"
extern PlayState* gPlayState; extern PlayState* gPlayState;
uint64_t GetUnixTimestamp();
} }
const std::vector<std::string> sceneMappings = { const char* const sceneMappings[] = {
{"Inside the Deku Tree"}, "Inside the Deku Tree",
{"Dodongo's Cavern"}, "Dodongo's Cavern",
{"Inside Jabu-Jabu's Belly"}, "Inside Jabu-Jabu's Belly",
{"Forest Temple"}, "Forest Temple",
{"Fire Temple"}, "Fire Temple",
{"Water Temple"}, "Water Temple",
{"Spirit Temple"}, "Spirit Temple",
{"Shadow Temple"}, "Shadow Temple",
{"Bottom of the Well"}, "Bottom of the Well",
{"Ice Cavern"}, "Ice Cavern",
{"Ganon's Tower"}, "Ganon's Tower",
{"Gerudo Training Ground"}, "Gerudo Training Ground",
{"Theives' Hideout"}, "Theives' Hideout",
{"Inside Ganon's Castle"}, "Inside Ganon's Castle",
{"Tower Collapse"}, "Tower Collapse",
{"Castle Collapse"}, "Castle Collapse",
{"Treasure Box Shop"}, "Treasure Box Shop",
{"Gohma's Lair"}, "Gohma's Lair",
{"King Dodongo's Lair"}, "King Dodongo's Lair",
{"Barinade's Lair"}, "Barinade's Lair",
{"Phantom Ganon's Lair"}, "Phantom Ganon's Lair",
{"Volvagia's Lair"}, "Volvagia's Lair",
{"Morpha's Lair"}, "Morpha's Lair",
{"Twinrova's Lair"}, "Twinrova's Lair",
{"Bongo Bongo's Lair"}, "Bongo Bongo's Lair",
{"Ganondorf's Lair"}, "Ganondorf's Lair",
{"Ganon's Lair"}, "Ganon's Lair",
{"Market Entrance (Day)"}, "Market Entrance (Day)",
{"Market Entrance (Night)"}, "Market Entrance (Night)",
{"Market Entrance (Adult)"}, "Market Entrance (Adult)",
{"Back Alley (Day)"}, "Back Alley (Day)",
{"Back Alley (Night)"}, "Back Alley (Night)",
{"Market (Day)"}, "Market (Day)",
{"Market (Night)"}, "Market (Night)",
{"Market (Adult)"}, "Market (Adult)",
{"Outside ToT (Day)"}, "Outside ToT (Day)",
{"Outside ToT (Night)"}, "Outside ToT (Night)",
{"Outside ToT (Adult)"}, "Outside ToT (Adult)",
{"Know-It-All Bros' House"}, "Know-It-All Bros' House",
{"Twins' House"}, "Twins' House",
{"Mido's House"}, "Mido's House",
{"Saria's House"}, "Saria's House",
{"Carpenter Boss's House"}, "Carpenter Boss's House",
{"Man in Green's House"}, "Man in Green's House",
{"Bazaar"}, "Bazaar",
{"Kokiri Shop"}, "Kokiri Shop",
{"Goron Shop"}, "Goron Shop",
{"Zora Shop"}, "Zora Shop",
{"Kakariko Potion Shop"}, "Kakariko Potion Shop",
{"Market Potion Shop"}, "Market Potion Shop",
{"Bombchu Shop"}, "Bombchu Shop",
{"Happy Mask Shop"}, "Happy Mask Shop",
{"Link's House"}, "Link's House",
{"Richard's House"}, "Richard's House",
{"Stable"}, "Stable",
{"Impa's House"}, "Impa's House",
{"Lakeside Lab"}, "Lakeside Lab",
{"Carpenters' Tent"}, "Carpenters' Tent",
{"Gravekeeper's Hut"}, "Gravekeeper's Hut",
{"Great Fairy"}, "Great Fairy",
{"Fairy Fountain"}, "Fairy Fountain",
{"Great Fairy"}, "Great Fairy",
{"Grotto"}, "Grotto",
{"Redead Grave"}, "Redead Grave",
{"Fairy Fountain Grave"}, "Fairy Fountain Grave",
{"Royal Family's Tomb"}, "Royal Family's Tomb",
{"Shooting Gallery"}, "Shooting Gallery",
{"Temple of Time"}, "Temple of Time",
{"Chamber of Sages"}, "Chamber of Sages",
{"Castle Maze (Day)"}, "Castle Maze (Day)",
{"Castle Maze (Night)"}, "Castle Maze (Night)",
{"Cutscene Map"}, "Cutscene Map",
{"Dampe's Grave"}, "Dampe's Grave",
{"Fishing Pond"}, "Fishing Pond",
{"Castle Courtyard"}, "Castle Courtyard",
{"Bombchu Bowling Alley"}, "Bombchu Bowling Alley",
{"Ranch House"}, "Ranch House",
{"Guard House"}, "Guard House",
{"Granny's Potion Shop"}, "Granny's Potion Shop",
{"Ganon Fight"}, "Ganon Fight",
{"House of Skulltula"}, "House of Skulltula",
{"Hyrule Field"}, "Hyrule Field",
{"Kakariko Village"}, "Kakariko Village",
{"Graveyard"}, "Graveyard",
{"Zora's River"}, "Zora's River",
{"Kokiri Forest"}, "Kokiri Forest",
{"Sacred Forest Meadow"}, "Sacred Forest Meadow",
{"Lake Hylia"}, "Lake Hylia",
{"Zora's Domain"}, "Zora's Domain",
{"Zora's Fountain"}, "Zora's Fountain",
{"Gerudo Valley"}, "Gerudo Valley",
{"Lost Woods"}, "Lost Woods",
{"Desert Colossus"}, "Desert Colossus",
{"Gerudo's Fortress"}, "Gerudo's Fortress",
{"Haunted Wasteland"}, "Haunted Wasteland",
{"Hyrule Castle"}, "Hyrule Castle",
{"Death Mountain Trail"}, "Death Mountain Trail",
{"Death Mountain Crater"}, "Death Mountain Crater",
{"Goron City"}, "Goron City",
{"Lon Lon Ranch"}, "Lon Lon Ranch",
{"Outside Ganon's Castle"}, "Outside Ganon's Castle",
//Debug Rooms //Debug Rooms
{"Test Map"}, "Test Map",
{"Test Room"}, "Test Room",
{"Depth Test"}, "Depth Test",
{"Stalfos Mini-Boss"}, "Stalfos Mini-Boss",
{"Stalfos Boss"}, "Stalfos Boss",
{"Dark Link"}, "Dark Link",
{"Castle Maze (Broken)"}, "Castle Maze (Broken)",
{"SRD Room"}, "SRD Room",
{"Chest Room"} "Chest Room",
};
const char* const countMappings[] = {
"Anubis:",
"Armos:",
"Arwing:",
"Bari:",
"Biri:",
"Beamos:",
"Big Octo:",
"Bubble (Blue):",
"Bubble (Green):",
"Bubble (Red):",
"Bubble (White):",
"Business Scrub:",
"Dark Link:",
"Dead Hand:",
"Deku Baba:",
"Deku Baba (Big):",
"Deku Scrub:",
"Dinolfos:",
"Dodongo:",
"Dodongo (Baby):",
"Door Mimic:",
"Flare Dancer:",
"Floormaster:",
"Flying Floor Tile:",
"Flying Pot:",
"Freezard:",
"Gerudo Thief:",
"Gibdo:",
"Gohma Larva:",
"Guay:",
"Iron Knuckle:",
"Iron Knuckle (Nab):",
"Keese:",
"Keese (Fire):",
"Keese (Ice):",
"Leever:",
"Leever (Big):",
"Like-Like:",
"Lizalfos:",
"Mad Scrub:",
"Moblin:",
"Moblin (Club):",
"Octorok:",
"Parasitic Tentacle:",
"Peahat:",
"Peahat Larva:",
"Poe:",
"Poe (Big):",
"Poe (Composer):",
"Poe Sisters:",
"Redead:",
"Shabom:",
"Shellblade:",
"Skull Kid:",
"Skulltula:",
"Skulltula (Big):",
"Skulltula (Gold):",
"Skullwalltula:",
"Spike:",
"Stalchild:",
"Stalfos:",
"Stinger:",
"Tailpasaran:",
"Tektite (Blue):",
"Tektite (Red):",
"Torch Slug:",
"Wallmaster:",
"Withered Deku Baba:",
"Wolfos:",
"Wolfos (White):",
"Deku Sticks:",
"Deku Nuts:",
"Bombs:",
"Arrows:",
"Deku Seeds:",
"Bombchus:",
"Beans:",
"A:",
"B:",
"L:",
"R:",
"Z:",
"C-Up:",
"C-Right:",
"C-Down:",
"C-Left:",
"D-Up:",
"D-Right:",
"D-Down:",
"D-Left:",
"Start:",
}; };
#define COLOR_WHITE ImVec4(1.00f, 1.00f, 1.00f, 1.00f) #define COLOR_WHITE ImVec4(1.00f, 1.00f, 1.00f, 1.00f)
@ -157,125 +252,134 @@ TimestampInfo itemTimestampDisplay[TIMESTAMP_MAX];
TimestampInfo sceneTimestampDisplay[8191]; TimestampInfo sceneTimestampDisplay[8191];
//std::vector<TimestampInfo> sceneTimestampDisplay; //std::vector<TimestampInfo> sceneTimestampDisplay;
void DisplayTimeHHMMSS(uint32_t timeInTenthsOfSeconds, std::string text, ImVec4 color) { std::string formatTimestampGameplayStat(uint32_t value) {
uint32_t sec = value / 10;
uint32_t sec = timeInTenthsOfSeconds / 10;
uint32_t hh = sec / 3600; uint32_t hh = sec / 3600;
uint32_t mm = (sec - hh * 3600) / 60; uint32_t mm = (sec - hh * 3600) / 60;
uint32_t ss = sec - hh * 3600 - mm * 60; uint32_t ss = sec - hh * 3600 - mm * 60;
uint32_t ds = timeInTenthsOfSeconds % 10; uint32_t ds = value % 10;
return fmt::format("{}:{:0>2}:{:0>2}.{}", hh, mm, ss, ds);
}
std::string formatIntGameplayStat(uint32_t value) {
return fmt::format("{}", value);
}
std::string formatHexGameplayStat(uint32_t value) {
return fmt::format("{:#x} ({:d})", value, value);
}
std::string formatHexOnlyGameplayStat(uint32_t value) {
return fmt::format("{:#x}", value, value);
}
void GameplayStatsRow(const char* label, std::string value, ImVec4 color = COLOR_WHITE) {
ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::TableNextRow();
std::string padded = fmt::format("{:<40}", text); ImGui::TableNextColumn();
ImGui::Text(padded.c_str()); ImGui::Text(label);
ImGui::SameLine(); ImGui::SameLine(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize(value.c_str()).x - 8.0f));
ImGui::Text("%2u:%02u:%02u.%u", hh, mm, ss, ds); ImGui::Text("%s", value.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
void SortChronological(TimestampInfo* arr, size_t len) { bool compareTimestampInfoByTime(const TimestampInfo& a, const TimestampInfo& b) {
TimestampInfo temp; return CVarGetInteger("gGameplayStats.TimestampsReverse", 0) ? a.time > b.time : a.time < b.time;
for (int i = 0; i < len; i++) {
for (int j = 0; j + 1 < len - i; j++) {
if (arr[j].time > arr[j + 1].time) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
} }
void DisplayStat(const char* text, uint32_t value) { const char* ResolveSceneID(int sceneID, int roomID){
ImGui::Text(text);
ImGui::SameLine();
ImGui::Text("%7u", value);
}
void DisplayStatIfNonZero(const char* text, uint32_t value) {
if (value > 0) {
DisplayStat(text, value);
}
return;
}
std::string ResolveSceneID(int sceneID, int roomID){
std::string scene = "";
if (sceneID == SCENE_KAKUSIANA) { if (sceneID == SCENE_KAKUSIANA) {
switch (roomID) { switch (roomID) {
case 0: case 0:
scene = "Generic Grotto"; return "Generic Grotto";
break;
case 1: case 1:
scene = "Lake Hylia Scrub Grotto"; return "Lake Hylia Scrub Grotto";
break;
case 2: case 2:
scene = "Redead Grotto"; return "Redead Grotto";
break;
case 3: case 3:
scene = "Cow Grotto"; return "Cow Grotto";
break;
case 4: case 4:
scene = "Scrub Trio"; return "Scrub Trio";
break;
case 5: case 5:
scene = "Flooded Grotto"; return "Flooded Grotto";
break;
case 6: case 6:
scene = "Scrub Duo (Upgrade)"; return "Scrub Duo (Upgrade)";
break;
case 7: case 7:
scene = "Wolfos Grotto"; return "Wolfos Grotto";
break;
case 8: case 8:
scene = "Hyrule Castle Storms Grotto"; return "Hyrule Castle Storms Grotto";
break;
case 9: case 9:
scene = "Scrub Duo"; return "Scrub Duo";
break;
case 10: case 10:
scene = "Tektite Grotto"; return "Tektite Grotto";
break;
case 11: case 11:
scene = "Forest Stage"; return "Forest Stage";
break;
case 12: case 12:
scene = "Webbed Grotto"; return "Webbed Grotto";
break;
case 13: case 13:
scene = "Big Skulltula Grotto"; return "Big Skulltula Grotto";
break;
default:
scene = "???";
}; };
} else if (sceneID == SCENE_HAKASITARELAY) { } else if (sceneID == SCENE_HAKASITARELAY) {
//Only the last room of Dampe's Grave (rm 6) is considered the windmill //Only the last room of Dampe's Grave (rm 6) is considered the windmill
scene = roomID == 6 ? "Windmill" : "Dampe's Grave"; return roomID == 6 ? "Windmill" : "Dampe's Grave";
} else if (sceneID < SCENE_ID_MAX) { } else if (sceneID < SCENE_ID_MAX) {
scene = sceneMappings[sceneID]; return sceneMappings[sceneID];
} else {
scene = "???";
} }
return scene;
return "???";
} }
void DrawStatsTracker(bool& open) { void DrawGameplayStatsHeader() {
if (!open) { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 4.0f, 4.0f });
if (CVarGetInteger("gGameplayStatsEnabled", 0)) { ImGui::BeginTable("gameplayStatsHeader", 1, ImGuiTableFlags_BordersOuter);
CVarClear("gGameplayStatsEnabled"); ImGui::TableSetupColumn("stat", ImGuiTableColumnFlags_WidthStretch);
LUS::RequestCvarSaveOnNextTick(); GameplayStatsRow("Build Version:", gSaveContext.sohStats.buildVersion);
} if (gSaveContext.sohStats.rtaTiming) {
return; GameplayStatsRow("Total Time (RTA):", formatTimestampGameplayStat(GAMEPLAYSTAT_TOTAL_TIME), gSaveContext.sohStats.gameComplete ? COLOR_GREEN : COLOR_WHITE);
} else {
GameplayStatsRow("Total Game Time:", formatTimestampGameplayStat(GAMEPLAYSTAT_TOTAL_TIME), gSaveContext.sohStats.gameComplete ? COLOR_GREEN : COLOR_WHITE);
}
if (CVarGetInteger("gGameplayStats.ShowAdditionalTimers", 0)) { // !Only display total game time
GameplayStatsRow("Gameplay Time:", formatTimestampGameplayStat(gSaveContext.sohStats.playTimer / 2), COLOR_GREY);
GameplayStatsRow("Pause Menu Time:", formatTimestampGameplayStat(gSaveContext.sohStats.pauseTimer / 3), COLOR_GREY);
GameplayStatsRow("Time in scene:", formatTimestampGameplayStat(gSaveContext.sohStats.sceneTimer / 2), COLOR_LIGHT_BLUE);
GameplayStatsRow("Time in room:", formatTimestampGameplayStat(gSaveContext.sohStats.roomTimer / 2), COLOR_LIGHT_BLUE);
}
if (gPlayState != NULL && CVarGetInteger("gGameplayStats.ShowDebugInfo", 0)) { // && display debug info
GameplayStatsRow("play->sceneNum:", formatHexGameplayStat(gPlayState->sceneNum), COLOR_YELLOW);
GameplayStatsRow("gSaveContext.entranceIndex:", formatHexGameplayStat(gSaveContext.entranceIndex), COLOR_YELLOW);
GameplayStatsRow("gSaveContext.cutsceneIndex:", formatHexOnlyGameplayStat(gSaveContext.cutsceneIndex), COLOR_YELLOW);
GameplayStatsRow("play->roomCtx.curRoom.num:", formatIntGameplayStat(gPlayState->roomCtx.curRoom.num), COLOR_YELLOW);
}
ImGui::EndTable();
ImGui::PopStyleVar(1);
}
void DrawGameplayStatsTimestampsTab() {
// Set up the array of item timestamps and then sort it chronologically
for (int i = 0; i < TIMESTAMP_MAX; i++) {
strcpy(itemTimestampDisplay[i].name, itemTimestampDisplayName[i]);
itemTimestampDisplay[i].time = gSaveContext.sohStats.itemTimestamp[i];
itemTimestampDisplay[i].color = itemTimestampDisplayColor[i];
} }
ImGui::SetNextWindowSize(ImVec2(480, 550), ImGuiCond_Appearing); std::sort(itemTimestampDisplay, itemTimestampDisplay + TIMESTAMP_MAX, compareTimestampInfoByTime);
if (!ImGui::Begin("Gameplay Stats", &open, ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::End(); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 4.0f, 4.0f });
return; ImGui::BeginTable("gameplayStatsTimestamps", 1, ImGuiTableFlags_BordersOuter);
ImGui::TableSetupColumn("stat", ImGuiTableColumnFlags_WidthStretch);
for (int i = 0; i < TIMESTAMP_MAX; i++) {
// To be shown, the entry must have a non-zero time and a string for its display name
if (itemTimestampDisplay[i].time > 0 && strnlen(itemTimestampDisplay[i].name, 21) > 1) {
GameplayStatsRow(itemTimestampDisplay[i].name, formatTimestampGameplayStat(itemTimestampDisplay[i].time), itemTimestampDisplay[i].color);
}
} }
u32 totalTimer = GAMEPLAYSTAT_TOTAL_TIME; ImGui::EndTable();
ImGui::PopStyleVar(1);
}
void DrawGameplayStatsCountsTab() {
u32 enemiesDefeated = 0; u32 enemiesDefeated = 0;
u32 ammoUsed = 0; u32 ammoUsed = 0;
u32 buttonPresses = 0; u32 buttonPresses = 0;
@ -297,230 +401,154 @@ void DrawStatsTracker(bool& open) {
for (int i = COUNT_BUTTON_PRESSES_A; i <= COUNT_BUTTON_PRESSES_START; i++) { for (int i = COUNT_BUTTON_PRESSES_A; i <= COUNT_BUTTON_PRESSES_START; i++) {
buttonPresses += gSaveContext.sohStats.count[i]; buttonPresses += gSaveContext.sohStats.count[i];
} }
// Set up the array of item timestamps and then sort it chronologically
for (int i = 0; i < TIMESTAMP_MAX; i++) { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 4.0f, 4.0f });
strcpy(itemTimestampDisplay[i].name, itemTimestampDisplayName[i]); ImGui::BeginTable("gameplayStatsCounts", 1, ImGuiTableFlags_BordersOuter);
itemTimestampDisplay[i].time = gSaveContext.sohStats.itemTimestamp[i]; ImGui::TableSetupColumn("stat", ImGuiTableColumnFlags_WidthStretch);
itemTimestampDisplay[i].color = itemTimestampDisplayColor[i]; GameplayStatsRow("Enemies Defeated:", formatIntGameplayStat(enemiesDefeated));
if (enemiesDefeated > 0) {
ImGui::TableNextRow(); ImGui::TableNextColumn();
if (ImGui::TreeNodeEx("Enemy Details...", ImGuiTreeNodeFlags_NoTreePushOnOpen)) {
for (int i = COUNT_ENEMIES_DEFEATED_ANUBIS; i <= COUNT_ENEMIES_DEFEATED_WOLFOS; i++) {
if (i == COUNT_ENEMIES_DEFEATED_FLOORMASTER) {
GameplayStatsRow(countMappings[i], formatIntGameplayStat(gSaveContext.sohStats.count[i] / 3));
} else {
GameplayStatsRow(countMappings[i], formatIntGameplayStat(gSaveContext.sohStats.count[i]));
}
}
}
} }
GameplayStatsRow("Rupees Collected:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_RUPEES_COLLECTED]));
UIWidgets::Tooltip("Includes rupees collected with a full wallet.");
GameplayStatsRow("Rupees Spent:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_RUPEES_SPENT]));
GameplayStatsRow("Chests Opened:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_CHESTS_OPENED]));
GameplayStatsRow("Ammo Used:", formatIntGameplayStat(ammoUsed));
if (ammoUsed > 0) {
ImGui::TableNextRow(); ImGui::TableNextColumn();
if (ImGui::TreeNodeEx("Ammo Details...", ImGuiTreeNodeFlags_NoTreePushOnOpen)) {
for (int i = COUNT_AMMO_USED_STICK; i <= COUNT_AMMO_USED_BEAN; i++) {
GameplayStatsRow(countMappings[i], formatIntGameplayStat(gSaveContext.sohStats.count[i]));
}
}
}
GameplayStatsRow("Damage Taken:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_DAMAGE_TAKEN]));
GameplayStatsRow("Sword Swings:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_SWORD_SWINGS]));
GameplayStatsRow("Steps Taken:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_STEPS]));
// If using MM Bunny Hood enhancement, show how long it's been equipped (not counting pause time)
if (CVarGetInteger("gMMBunnyHood", 0) || gSaveContext.sohStats.count[COUNT_TIME_BUNNY_HOOD] > 0) {
GameplayStatsRow("Bunny Hood Time:", formatTimestampGameplayStat(gSaveContext.sohStats.count[COUNT_TIME_BUNNY_HOOD] / 2));
}
GameplayStatsRow("Rolls:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_ROLLS]));
GameplayStatsRow("Bonks:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_BONKS]));
GameplayStatsRow("Sidehops:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_SIDEHOPS]));
GameplayStatsRow("Backflips:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_BACKFLIPS]));
GameplayStatsRow("Ice Traps:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_ICE_TRAPS]));
GameplayStatsRow("Pauses:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_PAUSES]));
GameplayStatsRow("Pots Smashed:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_POTS_BROKEN]));
GameplayStatsRow("Bushes Cut:", formatIntGameplayStat(gSaveContext.sohStats.count[COUNT_BUSHES_CUT]));
GameplayStatsRow("Buttons Pressed:", formatIntGameplayStat(buttonPresses));
if (buttonPresses > 0) {
ImGui::TableNextRow(); ImGui::TableNextColumn();
if (ImGui::TreeNodeEx("Buttons...", ImGuiTreeNodeFlags_NoTreePushOnOpen)) {
for (int i = COUNT_BUTTON_PRESSES_A; i <= COUNT_BUTTON_PRESSES_START; i++) {
GameplayStatsRow(countMappings[i], formatIntGameplayStat(gSaveContext.sohStats.count[i]));
}
}
}
ImGui::EndTable();
ImGui::PopStyleVar(1);
}
void DrawGameplayStatsBreakdownTab() {
for (int i = 0; i < gSaveContext.sohStats.tsIdx; i++) { for (int i = 0; i < gSaveContext.sohStats.tsIdx; i++) {
std::string sceneName = ResolveSceneID(gSaveContext.sohStats.sceneTimestamps[i].scene, gSaveContext.sohStats.sceneTimestamps[i].room); std::string sceneName = ResolveSceneID(gSaveContext.sohStats.sceneTimestamps[i].scene, gSaveContext.sohStats.sceneTimestamps[i].room);
std::string name; std::string name;
if (CVarGetInteger("gGameplayStatRoomBreakdown", 0) && gSaveContext.sohStats.sceneTimestamps[i].scene != SCENE_KAKUSIANA) { if (CVarGetInteger("gGameplayStats.RoomBreakdown", 0) && gSaveContext.sohStats.sceneTimestamps[i].scene != SCENE_KAKUSIANA) {
name = fmt::format("{:s} Room {:d}", sceneName, gSaveContext.sohStats.sceneTimestamps[i].room); name = fmt::format("{:s} Room {:d}", sceneName, gSaveContext.sohStats.sceneTimestamps[i].room);
} else { } else {
name = sceneName; name = sceneName;
} }
strcpy(sceneTimestampDisplay[i].name, name.c_str()); strcpy(sceneTimestampDisplay[i].name, name.c_str());
sceneTimestampDisplay[i].time = CVarGetInteger("gGameplayStatRoomBreakdown", 0) ? sceneTimestampDisplay[i].time = CVarGetInteger("gGameplayStats.RoomBreakdown", 0) ?
gSaveContext.sohStats.sceneTimestamps[i].roomTime : gSaveContext.sohStats.sceneTimestamps[i].sceneTime; gSaveContext.sohStats.sceneTimestamps[i].roomTime : gSaveContext.sohStats.sceneTimestamps[i].sceneTime;
sceneTimestampDisplay[i].color = COLOR_GREY; sceneTimestampDisplay[i].color = COLOR_GREY;
sceneTimestampDisplay[i].isRoom = gSaveContext.sohStats.sceneTimestamps[i].isRoom; sceneTimestampDisplay[i].isRoom = gSaveContext.sohStats.sceneTimestamps[i].isRoom;
} }
SortChronological(itemTimestampDisplay, sizeof(itemTimestampDisplay) / sizeof(itemTimestampDisplay[0])); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 4.0f, 4.0f });
ImGui::BeginTable("gameplayStatsCounts", 1, ImGuiTableFlags_BordersOuter);
ImGui::TableSetupColumn("stat", ImGuiTableColumnFlags_WidthStretch);
// Begin drawing the table and showing the stats for (int i = 0; i < gSaveContext.sohStats.tsIdx; i++) {
TimestampInfo tsInfo = sceneTimestampDisplay[i];
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 8.0f, 8.0f }); bool canShow = !tsInfo.isRoom || CVarGetInteger("gGameplayStats.RoomBreakdown", 0);
ImGui::BeginTable("timers", 1, ImGuiTableFlags_BordersH | ImGuiTableFlags_BordersV); if (tsInfo.time > 0 && strnlen(tsInfo.name, 40) > 1 && canShow) {
ImGui::TableSetupColumn("Timers", ImGuiTableColumnFlags_WidthStretch, 200.0f); GameplayStatsRow(tsInfo.name, formatTimestampGameplayStat(tsInfo.time), tsInfo.color);
ImGui::TableNextColumn(); }
}
DisplayTimeHHMMSS(totalTimer, "Total Game Time: ", COLOR_WHITE); std::string toPass;
UIWidgets::Tooltip("Timer accuracy may be affected by game performance and loading."); if (CVarGetInteger("gGameplayStats.RoomBreakdown", 0) && gSaveContext.sohStats.sceneNum != SCENE_KAKUSIANA) {
DisplayTimeHHMMSS(gSaveContext.sohStats.playTimer / 2, "Gameplay Time: ", COLOR_WHITE); toPass = fmt::format("{:s} Room {:d}", ResolveSceneID(gSaveContext.sohStats.sceneNum, gSaveContext.sohStats.roomNum), gSaveContext.sohStats.roomNum);
UIWidgets::Tooltip("Timer accuracy may be affected by game performance and loading."); } else {
DisplayTimeHHMMSS(gSaveContext.sohStats.pauseTimer / 3, "Pause Menu Time: ", COLOR_WHITE); toPass = ResolveSceneID(gSaveContext.sohStats.sceneNum, gSaveContext.sohStats.roomNum);
DisplayTimeHHMMSS(gSaveContext.sohStats.sceneTimer / 2, "Time in scene: ", COLOR_LIGHT_BLUE); }
UIWidgets::Tooltip("Timer accuracy may be affected by game performance and loading."); GameplayStatsRow(toPass.c_str(), formatTimestampGameplayStat(CURRENT_MODE_TIMER / 2));
DisplayTimeHHMMSS(gSaveContext.sohStats.roomTimer / 2, "Time in room: ", COLOR_LIGHT_BLUE);
UIWidgets::Tooltip("Timer accuracy may be affected by game performance and loading.");
ImGui::Text("Current room: %d", gSaveContext.sohStats.roomNum);
ImGui::PopStyleVar(1);
ImGui::EndTable(); ImGui::EndTable();
ImGui::PopStyleVar(1);
}
void DrawGameplayStatsOptionsTab() {
UIWidgets::PaddedEnhancementCheckbox("Show latest timestamps on top", "gGameplayStats.TimestampsReverse");
UIWidgets::PaddedEnhancementCheckbox("Room Breakdown", "gGameplayStats.RoomBreakdown");
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");
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 Debug Info", "gGameplayStats.ShowDebugInfo");
}
void DrawStatsTracker(bool& open) {
if (!open) {
if (CVarGetInteger("gGameplayStatsEnabled", 0)) {
CVarClear("gGameplayStatsEnabled");
LUS::RequestCvarSaveOnNextTick();
}
return;
}
ImGui::SetNextWindowSize(ImVec2(480, 550), ImGuiCond_Appearing);
if (!ImGui::Begin("Gameplay Stats", &open, ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::End();
return;
}
DrawGameplayStatsHeader();
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 8.0f, 8.0f });
if (ImGui::BeginTabBar("Stats", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) { if (ImGui::BeginTabBar("Stats", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) {
if (ImGui::BeginTabItem("Timestamps")) { if (ImGui::BeginTabItem("Timestamps")) {
// Display chronological timestamps of items obtained and bosses defeated DrawGameplayStatsTimestampsTab();
for (int i = 0; i < TIMESTAMP_MAX; i++) {
// To be shown, the entry must have a non-zero time and a string for its display name
if (itemTimestampDisplay[i].time > 0 && strnlen(itemTimestampDisplay[i].name, 21) > 1) {
DisplayTimeHHMMSS(itemTimestampDisplay[i].time, itemTimestampDisplay[i].name, itemTimestampDisplay[i].color);
}
}
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem("Counts")) { if (ImGui::BeginTabItem("Counts")) {
DisplayStat("Enemies Defeated: ", enemiesDefeated); DrawGameplayStatsCountsTab();
// Show breakdown of enemies defeated in a tree. Only show counts for enemies if they've been defeated at least once.
if (enemiesDefeated > 0) {
if (ImGui::TreeNode("Enemy Details...")) {
DisplayStatIfNonZero("Anubis: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_ANUBIS]);
DisplayStatIfNonZero("Armos: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_ARMOS]);
DisplayStatIfNonZero("Arwing: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_ARWING]);
DisplayStatIfNonZero("Bari: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BARI]);
DisplayStatIfNonZero("Biri: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BIRI]);
DisplayStatIfNonZero("Beamos: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BEAMOS]);
DisplayStatIfNonZero("Big Octo: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BIG_OCTO]);
DisplayStatIfNonZero("Bubble (Blue): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BUBBLE_BLUE]);
DisplayStatIfNonZero("Bubble (Green): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BUBBLE_GREEN]);
DisplayStatIfNonZero("Bubble (Red): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BUBBLE_RED]);
DisplayStatIfNonZero("Bubble (White): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BUBBLE_WHITE]);
DisplayStatIfNonZero("Business Scrub: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_BUSINESS_SCRUB]);
DisplayStatIfNonZero("Dark Link: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DARK_LINK]);
DisplayStatIfNonZero("Dead Hand: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DEAD_HAND]);
DisplayStatIfNonZero("Deku Baba: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DEKU_BABA]);
DisplayStatIfNonZero("Deku Baba (Big): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DEKU_BABA_BIG]);
DisplayStatIfNonZero("Deku Scrub: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DEKU_SCRUB]);
DisplayStatIfNonZero("Dinolfos: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DINOLFOS]);
DisplayStatIfNonZero("Dodongo: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DODONGO]);
DisplayStatIfNonZero("Dodongo (Baby): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DODONGO_BABY]);
DisplayStatIfNonZero("Door Mimic: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_DOOR_TRAP]);
DisplayStatIfNonZero("Flare Dancer: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_FLARE_DANCER]);
DisplayStatIfNonZero("Floormaster: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_FLOORMASTER]/3);
DisplayStatIfNonZero("Flying Floor Tile: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_FLOOR_TILE]);
DisplayStatIfNonZero("Flying Pot: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_FLYING_POT]);
DisplayStatIfNonZero("Freezard: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_FREEZARD]);
DisplayStatIfNonZero("Gerudo Thief: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_GERUDO_THIEF]);
DisplayStatIfNonZero("Gibdo: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_GIBDO]);
DisplayStatIfNonZero("Gohma Larva: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_GOHMA_LARVA]);
DisplayStatIfNonZero("Guay: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_GUAY]);
DisplayStatIfNonZero("Iron Knuckle: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_IRON_KNUCKLE]);
DisplayStatIfNonZero("Iron Knuckle (Nab): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_IRON_KNUCKLE_NABOORU]);
DisplayStatIfNonZero("Keese: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_KEESE]);
DisplayStatIfNonZero("Keese (Fire): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_KEESE_FIRE]);
DisplayStatIfNonZero("Keese (Ice): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_KEESE_ICE]);
DisplayStatIfNonZero("Leever: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_LEEVER]);
DisplayStatIfNonZero("Leever (Big): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_LEEVER_BIG]);
DisplayStatIfNonZero("Like-Like: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_LIKE_LIKE]);
DisplayStatIfNonZero("Lizalfos: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_LIZALFOS]);
DisplayStatIfNonZero("Mad Scrub: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_MAD_SCRUB]);
DisplayStatIfNonZero("Moblin: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_MOBLIN]);
DisplayStatIfNonZero("Moblin (Club): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_MOBLIN_CLUB]);
DisplayStatIfNonZero("Octorok: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_OCTOROK]);
DisplayStatIfNonZero("Parasitic Tentacle: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_PARASITIC_TENTACLE]);
DisplayStatIfNonZero("Peahat: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_PEAHAT]);
DisplayStatIfNonZero("Peahat Larva: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_PEAHAT_LARVA]);
DisplayStatIfNonZero("Poe: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_POE]);
DisplayStatIfNonZero("Poe (Big): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_POE_BIG]);
DisplayStatIfNonZero("Poe (Composer): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_POE_COMPOSER]);
DisplayStatIfNonZero("Poe Sisters: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_POE_SISTERS]);
DisplayStatIfNonZero("Redead: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_REDEAD]);
DisplayStatIfNonZero("Shabom: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SHABOM]);
DisplayStatIfNonZero("Shellblade: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SHELLBLADE]);
DisplayStatIfNonZero("Skull Kid: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SKULL_KID]);
DisplayStatIfNonZero("Skulltula: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SKULLTULA]);
DisplayStatIfNonZero("Skulltula (Big): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SKULLTULA_BIG]);
DisplayStatIfNonZero("Skulltula (Gold): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SKULLTULA_GOLD]);
DisplayStatIfNonZero("Skullwalltula: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SKULLWALLTULA]);
DisplayStatIfNonZero("Spike: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_SPIKE]);
DisplayStatIfNonZero("Stalchild: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_STALCHILD]);
DisplayStatIfNonZero("Stalfos: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_STALFOS]);
DisplayStatIfNonZero("Stinger: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_STINGER]);
DisplayStatIfNonZero("Tailpasaran: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_TAILPASARAN]);
DisplayStatIfNonZero("Tektite (Blue): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_TEKTITE_BLUE]);
DisplayStatIfNonZero("Tektite (Red): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_TEKTITE_RED]);
DisplayStatIfNonZero("Torch Slug: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_TORCH_SLUG]);
DisplayStatIfNonZero("Wallmaster: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_WALLMASTER]);
DisplayStatIfNonZero("Withered Deku Baba: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_WITHERED_DEKU_BABA]);
DisplayStatIfNonZero("Wolfos: ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_WOLFOS]);
DisplayStatIfNonZero("Wolfos (White): ", gSaveContext.sohStats.count[COUNT_ENEMIES_DEFEATED_WOLFOS_WHITE]);
ImGui::NewLine();
ImGui::TreePop();
}
}
DisplayStat("Rupees Collected: ", gSaveContext.sohStats.count[COUNT_RUPEES_COLLECTED]);
UIWidgets::Tooltip("Includes rupees collected with a full wallet.");
DisplayStat("Rupees Spent: ", gSaveContext.sohStats.count[COUNT_RUPEES_SPENT]);
DisplayStat("Chests Opened: ", gSaveContext.sohStats.count[COUNT_CHESTS_OPENED]);
DisplayStat("Ammo Used: ", ammoUsed);
// Show breakdown of ammo used in a collapsible tree. Only show ammo types if they've been used at least once.
if (ammoUsed > 0) {
if (ImGui::TreeNode("Ammo Details...")) {
DisplayStatIfNonZero("Deku Sticks: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_STICK]);
DisplayStatIfNonZero("Deku Nuts: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_NUT]);
DisplayStatIfNonZero("Deku Seeds: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_SEED]);
DisplayStatIfNonZero("Bombs: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_BOMB]);
DisplayStatIfNonZero("Bombchus: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_BOMBCHU]);
DisplayStatIfNonZero("Arrows: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_ARROW]);
DisplayStatIfNonZero("Beans: ", gSaveContext.sohStats.count[COUNT_AMMO_USED_BEAN]);
ImGui::NewLine();
ImGui::TreePop();
}
}
DisplayStat("Damage Taken: ", gSaveContext.sohStats.count[COUNT_DAMAGE_TAKEN]);
DisplayStat("Sword Swings: ", gSaveContext.sohStats.count[COUNT_SWORD_SWINGS]);
DisplayStat("Steps Taken: ", gSaveContext.sohStats.count[COUNT_STEPS]);
// If using MM Bunny Hood enhancement, show how long it's been equipped (not counting pause time)
if (CVarGetInteger("gMMBunnyHood", 0) || gSaveContext.sohStats.count[COUNT_TIME_BUNNY_HOOD] > 0) {
DisplayTimeHHMMSS(gSaveContext.sohStats.count[COUNT_TIME_BUNNY_HOOD] / 2, "Bunny Hood Time: ", COLOR_WHITE);
}
DisplayStat("Rolls: ", gSaveContext.sohStats.count[COUNT_ROLLS]);
DisplayStat("Bonks: ", gSaveContext.sohStats.count[COUNT_BONKS]);
DisplayStat("Sidehops: ", gSaveContext.sohStats.count[COUNT_SIDEHOPS]);
DisplayStat("Backflips: ", gSaveContext.sohStats.count[COUNT_BACKFLIPS]);
DisplayStat("Ice Traps: ", gSaveContext.sohStats.count[COUNT_ICE_TRAPS]);
DisplayStat("Pauses: ", gSaveContext.sohStats.count[COUNT_PAUSES]);
DisplayStat("Pots Smashed: ", gSaveContext.sohStats.count[COUNT_POTS_BROKEN]);
DisplayStat("Bushes Cut: ", gSaveContext.sohStats.count[COUNT_BUSHES_CUT]);
DisplayStat("Buttons Pressed: ", buttonPresses);
// Show breakdown of ammo used in a collapsible tree. Only show ammo types if they've been used at least once.
if (buttonPresses > 0) {
if (ImGui::TreeNode("Buttons...")) {
DisplayStatIfNonZero("A: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_A]);
DisplayStatIfNonZero("B: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_B]);
DisplayStatIfNonZero("L: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_L]);
DisplayStatIfNonZero("R: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_R]);
DisplayStatIfNonZero("Z: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_Z]);
DisplayStatIfNonZero("C-Up: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_CUP]);
DisplayStatIfNonZero("C-Right: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_CRIGHT]);
DisplayStatIfNonZero("C-Down: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_CDOWN]);
DisplayStatIfNonZero("C-Left: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_CLEFT]);
DisplayStatIfNonZero("D-Up: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_DUP]);
DisplayStatIfNonZero("D-Right: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_DRIGHT]);
DisplayStatIfNonZero("D-Down: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_DDOWN]);
DisplayStatIfNonZero("D-Left: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_DLEFT]);
DisplayStatIfNonZero("Start: ", gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_START]);
ImGui::NewLine();
ImGui::TreePop();
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Breakdown")) {
UIWidgets::PaddedEnhancementCheckbox("Room Breakdown", "gGameplayStatRoomBreakdown");
ImGui::SameLine();
UIWidgets::InsertHelpHoverText("Allows a more in-depth perspective of time spent in a certain map.");
if (gPlayState == NULL) {
ImGui::Text("Waiting for file load...");
} else {
for (int i = 0; i < gSaveContext.sohStats.tsIdx; i++) {
TimestampInfo tsInfo = sceneTimestampDisplay[i];
bool canShow = !tsInfo.isRoom || CVarGetInteger("gGameplayStatRoomBreakdown", 0);
if (tsInfo.time > 0 && strnlen(tsInfo.name, 40) > 1 && canShow) {
DisplayTimeHHMMSS(tsInfo.time, tsInfo.name, tsInfo.color);
}
}
std::string toPass;
if (CVarGetInteger("gGameplayStatRoomBreakdown", 0) && gSaveContext.sohStats.sceneNum != SCENE_KAKUSIANA) {
toPass = fmt::format("{:s} Room {:d}", ResolveSceneID(gSaveContext.sohStats.sceneNum, gSaveContext.sohStats.roomNum), gSaveContext.sohStats.roomNum);
} else {
toPass = ResolveSceneID(gSaveContext.sohStats.sceneNum, gSaveContext.sohStats.roomNum);
}
DisplayTimeHHMMSS(CURRENT_MODE_TIMER / 2, toPass.c_str(), COLOR_WHITE);
}
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
ImGui::EndTabBar(); if (ImGui::BeginTabItem("Breakdown")) {
DrawGameplayStatsBreakdownTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Options")) {
DrawGameplayStatsOptionsTab();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
} }
ImGui::PopStyleVar(1);
ImGui::Text("Note: Gameplay stats are saved to the current file and will be\nlost if you quit without saving."); ImGui::Text("Note: Gameplay stats are saved to the current file and will be\nlost if you quit without saving.");
ImGui::End(); ImGui::End();
@ -670,7 +698,7 @@ void SetupDisplayColors() {
} }
extern "C" void InitStatTracker() { extern "C" void InitStatTracker() {
LUS::AddWindow("Enhancements", "Gameplay Stats", DrawStatsTracker, CVarGetInteger("gGameplayStatsEnabled", 0)); LUS::AddWindow("Enhancements", "Gameplay Stats", DrawStatsTracker, CVarGetInteger("gGameplayStats.Enabled", 0));
SetupDisplayNames(); SetupDisplayNames();
SetupDisplayColors(); SetupDisplayColors();
} }

View File

@ -1,10 +1,18 @@
#pragma once #pragma once
// Total gameplay time is tracked in tenths of seconds // When using RTA timing
// I.E. game time counts frames at 20fps/2, pause time counts frames at 30fps/3 // get the diff since the save was created,
// Frame counts in z_play.c and z_kaleido_scope_call.c // unless the game is complete in which we use the defeated ganon timestamp
#define GAMEPLAYSTAT_TOTAL_TIME (gSaveContext.sohStats.playTimer / 2 + gSaveContext.sohStats.pauseTimer / 3) // When not using RTA timing
#define CURRENT_MODE_TIMER (CVarGetInteger("gGameplayStatRoomBreakdown", 0) ?\ // Total gameplay time is tracked in tenths of seconds
// I.E. game time counts frames at 20fps/2, pause time counts frames at 30fps/3
// Frame counts in z_play.c and z_kaleido_scope_call.c
#define GAMEPLAYSTAT_TOTAL_TIME (gSaveContext.sohStats.rtaTiming ?\
(!gSaveContext.sohStats.gameComplete ?\
(!gSaveContext.sohStats.fileCreatedAt ? 0 : ((GetUnixTimestamp() - gSaveContext.sohStats.fileCreatedAt) / 100)) :\
(gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GANON])) :\
(gSaveContext.sohStats.playTimer / 2 + gSaveContext.sohStats.pauseTimer / 3))
#define CURRENT_MODE_TIMER (CVarGetInteger("gGameplayStats.RoomBreakdown", 0) ?\
gSaveContext.sohStats.roomTimer :\ gSaveContext.sohStats.roomTimer :\
gSaveContext.sohStats.sceneTimer) gSaveContext.sohStats.sceneTimer)

View File

@ -1013,14 +1013,14 @@ namespace GameMenuBar {
LUS::RequestCvarSaveOnNextTick(); LUS::RequestCvarSaveOnNextTick();
LUS::EnableWindow("Audio Editor", CVarGetInteger("gAudioEditor.WindowOpen", 0)); LUS::EnableWindow("Audio Editor", CVarGetInteger("gAudioEditor.WindowOpen", 0));
} }
if (ImGui::Button(GetWindowButtonText("Gameplay Stats", CVarGetInteger("gGameplayStatsEnabled", 0)).c_str(), ImVec2(-1.0f, 0.0f))) { if (ImGui::Button(GetWindowButtonText("Gameplay Stats", CVarGetInteger("gGameplayStats.Enabled", 0)).c_str(), ImVec2(-1.0f, 0.0f))) {
if (CVarGetInteger("gGameplayStatsEnabled", 0)) { if (CVarGetInteger("gGameplayStats.Enabled", 0)) {
CVarClear("gGameplayStatsEnabled"); CVarClear("gGameplayStats.Enabled");
} else { } else {
CVarSetInteger("gGameplayStatsEnabled", 1); CVarSetInteger("gGameplayStats.Enabled", 1);
} }
LUS::RequestCvarSaveOnNextTick(); LUS::RequestCvarSaveOnNextTick();
LUS::EnableWindow("Gameplay Stats", CVarGetInteger("gGameplayStatsEnabled", 0)); LUS::EnableWindow("Gameplay Stats", CVarGetInteger("gGameplayStats.Enabled", 0));
} }
ImGui::PopStyleVar(3); ImGui::PopStyleVar(3);
ImGui::PopStyleColor(1); ImGui::PopStyleColor(1);

View File

@ -4,6 +4,7 @@
#include <algorithm> #include <algorithm>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <chrono>
#include <ResourceManager.h> #include <ResourceManager.h>
#include <File.h> #include <File.h>
@ -831,6 +832,14 @@ extern "C" uint64_t GetPerfCounter() {
} }
#endif #endif
extern "C" uint64_t GetUnixTimestamp() {
auto time = std::chrono::system_clock::now();
auto since_epoch = time.time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(since_epoch);
long now = millis.count();
return now;
}
// C->C++ Bridge // C->C++ Bridge
extern "C" void Graph_ProcessFrame(void (*run_one_game_iter)(void)) { extern "C" void Graph_ProcessFrame(void (*run_one_game_iter)(void)) {
OTRGlobals::Instance->context->GetWindow()->MainLoop(run_one_game_iter); OTRGlobals::Instance->context->GetWindow()->MainLoop(run_one_game_iter);

View File

@ -88,6 +88,7 @@ void Ctx_ReadSaveFile(uintptr_t addr, void* dramAddr, size_t size);
void Ctx_WriteSaveFile(uintptr_t addr, void* dramAddr, size_t size); void Ctx_WriteSaveFile(uintptr_t addr, void* dramAddr, size_t size);
uint64_t GetPerfCounter(); uint64_t GetPerfCounter();
uint64_t GetUnixTimestamp();
struct SkeletonHeader* ResourceMgr_LoadSkeletonByName(const char* path, SkelAnime* skelAnime); struct SkeletonHeader* ResourceMgr_LoadSkeletonByName(const char* path, SkelAnime* skelAnime);
void ResourceMgr_UnregisterSkeleton(SkelAnime* skelAnime); void ResourceMgr_UnregisterSkeleton(SkelAnime* skelAnime);
void ResourceMgr_ClearSkeletons(); void ResourceMgr_ClearSkeletons();

View File

@ -512,6 +512,8 @@ void SaveManager::InitFileNormal() {
for (int dungeon = 0; dungeon < ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys); dungeon++) { for (int dungeon = 0; dungeon < ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys); dungeon++) {
gSaveContext.sohStats.dungeonKeys[dungeon] = 0; gSaveContext.sohStats.dungeonKeys[dungeon] = 0;
} }
gSaveContext.sohStats.rtaTiming = CVarGetInteger("gGameplayStats.RTATiming", 0);
gSaveContext.sohStats.fileCreatedAt = 0;
gSaveContext.sohStats.playTimer = 0; gSaveContext.sohStats.playTimer = 0;
gSaveContext.sohStats.pauseTimer = 0; gSaveContext.sohStats.pauseTimer = 0;
for (int timestamp = 0; timestamp < ARRAY_COUNT(gSaveContext.sohStats.itemTimestamp); timestamp++) { for (int timestamp = 0; timestamp < ARRAY_COUNT(gSaveContext.sohStats.itemTimestamp); timestamp++) {
@ -1112,6 +1114,8 @@ void SaveManager::LoadBaseVersion2() {
SaveManager::Instance->LoadArray("dungeonKeys", ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys), [](size_t i) { SaveManager::Instance->LoadArray("dungeonKeys", ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys), [](size_t i) {
SaveManager::Instance->LoadData("", gSaveContext.sohStats.dungeonKeys[i]); SaveManager::Instance->LoadData("", gSaveContext.sohStats.dungeonKeys[i]);
}); });
SaveManager::Instance->LoadData("rtaTiming", gSaveContext.sohStats.rtaTiming);
SaveManager::Instance->LoadData("fileCreatedAt", gSaveContext.sohStats.fileCreatedAt);
SaveManager::Instance->LoadData("playTimer", gSaveContext.sohStats.playTimer); SaveManager::Instance->LoadData("playTimer", gSaveContext.sohStats.playTimer);
SaveManager::Instance->LoadData("pauseTimer", gSaveContext.sohStats.pauseTimer); SaveManager::Instance->LoadData("pauseTimer", gSaveContext.sohStats.pauseTimer);
SaveManager::Instance->LoadArray("timestamps", ARRAY_COUNT(gSaveContext.sohStats.itemTimestamp), [](size_t i) { SaveManager::Instance->LoadArray("timestamps", ARRAY_COUNT(gSaveContext.sohStats.itemTimestamp), [](size_t i) {
@ -1326,6 +1330,8 @@ void SaveManager::LoadBaseVersion3() {
SaveManager::Instance->LoadArray("dungeonKeys", ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys), [](size_t i) { SaveManager::Instance->LoadArray("dungeonKeys", ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys), [](size_t i) {
SaveManager::Instance->LoadData("", gSaveContext.sohStats.dungeonKeys[i]); SaveManager::Instance->LoadData("", gSaveContext.sohStats.dungeonKeys[i]);
}); });
SaveManager::Instance->LoadData("rtaTiming", gSaveContext.sohStats.rtaTiming);
SaveManager::Instance->LoadData("fileCreatedAt", gSaveContext.sohStats.fileCreatedAt);
SaveManager::Instance->LoadData("playTimer", gSaveContext.sohStats.playTimer); SaveManager::Instance->LoadData("playTimer", gSaveContext.sohStats.playTimer);
SaveManager::Instance->LoadData("pauseTimer", gSaveContext.sohStats.pauseTimer); SaveManager::Instance->LoadData("pauseTimer", gSaveContext.sohStats.pauseTimer);
SaveManager::Instance->LoadArray("itemTimestamps", ARRAY_COUNT(gSaveContext.sohStats.itemTimestamp), [](size_t i) { SaveManager::Instance->LoadArray("itemTimestamps", ARRAY_COUNT(gSaveContext.sohStats.itemTimestamp), [](size_t i) {
@ -1535,6 +1541,8 @@ void SaveManager::SaveBase(SaveContext* saveContext) {
SaveManager::Instance->SaveArray("dungeonKeys", ARRAY_COUNT(saveContext->sohStats.dungeonKeys), [&](size_t i) { SaveManager::Instance->SaveArray("dungeonKeys", ARRAY_COUNT(saveContext->sohStats.dungeonKeys), [&](size_t i) {
SaveManager::Instance->SaveData("", saveContext->sohStats.dungeonKeys[i]); SaveManager::Instance->SaveData("", saveContext->sohStats.dungeonKeys[i]);
}); });
SaveManager::Instance->SaveData("rtaTiming", saveContext->sohStats.rtaTiming);
SaveManager::Instance->SaveData("fileCreatedAt", saveContext->sohStats.fileCreatedAt);
SaveManager::Instance->SaveData("playTimer", saveContext->sohStats.playTimer); SaveManager::Instance->SaveData("playTimer", saveContext->sohStats.playTimer);
SaveManager::Instance->SaveData("pauseTimer", saveContext->sohStats.pauseTimer); SaveManager::Instance->SaveData("pauseTimer", saveContext->sohStats.pauseTimer);
SaveManager::Instance->SaveArray("itemTimestamps", ARRAY_COUNT(saveContext->sohStats.itemTimestamp), [&](size_t i) { SaveManager::Instance->SaveArray("itemTimestamps", ARRAY_COUNT(saveContext->sohStats.itemTimestamp), [&](size_t i) {

View File

@ -751,6 +751,14 @@ void Play_Update(PlayState* play) {
if (CHECK_BTN_ALL(input[0].press.button, BTN_R)) {gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_R]++;} if (CHECK_BTN_ALL(input[0].press.button, BTN_R)) {gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_R]++;}
if (CHECK_BTN_ALL(input[0].press.button, BTN_Z)) {gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_Z]++;} if (CHECK_BTN_ALL(input[0].press.button, BTN_Z)) {gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_Z]++;}
if (CHECK_BTN_ALL(input[0].press.button, BTN_START)) {gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_START]++;} if (CHECK_BTN_ALL(input[0].press.button, BTN_START)) {gSaveContext.sohStats.count[COUNT_BUTTON_PRESSES_START]++;}
// Start RTA timing on first non-c-up input after intro cutscene
if (
!gSaveContext.sohStats.fileCreatedAt && !Player_InCsMode(play) &&
((input[0].press.button && input[0].press.button != 0x8) || input[0].rel.stick_x != 0 || input[0].rel.stick_y != 0)
) {
gSaveContext.sohStats.fileCreatedAt = GetUnixTimestamp();
}
} }
if (gTrnsnUnkState != 0) { if (gTrnsnUnkState != 0) {

View File

@ -1680,8 +1680,8 @@ void func_8090120C(BossGanon2* this, PlayState* play) {
if ((ABS(temp_a0_2) < 0x2000) && (sqrtf(SQ(temp_f14) + SQ(temp_f12)) < 70.0f) && if ((ABS(temp_a0_2) < 0x2000) && (sqrtf(SQ(temp_f14) + SQ(temp_f12)) < 70.0f) &&
(player->swordState != 0) && (player->heldItemAction == PLAYER_IA_SWORD_MASTER)) { (player->swordState != 0) && (player->heldItemAction == PLAYER_IA_SWORD_MASTER)) {
func_80064520(play, &play->csCtx); func_80064520(play, &play->csCtx);
gSaveContext.sohStats.gameComplete = true;
gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME; gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME;
gSaveContext.sohStats.gameComplete = true;
this->unk_39E = Play_CreateSubCamera(play); this->unk_39E = Play_CreateSubCamera(play);
Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_WAIT); Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_WAIT);
Play_ChangeCameraStatus(play, this->unk_39E, CAM_STAT_ACTIVE); Play_ChangeCameraStatus(play, this->unk_39E, CAM_STAT_ACTIVE);