diff --git a/AggregatedStats.cpp b/AggregatedStats.cpp index aafa83c..414ad98 100644 --- a/AggregatedStats.cpp +++ b/AggregatedStats.cpp @@ -10,6 +10,9 @@ #include #include +constexpr const char* GROUP_FILTER_STRING[] = { "Group", "Squad", "All (Excluding Summons)", "All (Including Summons)" }; +static_assert((sizeof(GROUP_FILTER_STRING) / sizeof(GROUP_FILTER_STRING[0])) == static_cast(GroupFilter::Max), "Added group filter option without updating gui?"); + AggregatedStatsEntry::AggregatedStatsEntry(uint64_t pId, std::string&& pName, uint64_t pHealing, uint64_t pHits, std::optional pCasts) : Id{pId} , Name{pName} @@ -25,6 +28,7 @@ AggregatedStats::AggregatedStats(HealingStats&& pSourceData, const HealWindowOpt , myAllAgents(nullptr) , myFilteredAgents(nullptr) , mySkills(nullptr) + , myGroupFilterTotals(nullptr) , myDebugMode(pDebugMode) { assert(static_cast(myOptions.SortOrderChoice) < SortOrder::Max); @@ -57,13 +61,15 @@ const AggregatedStatsEntry& AggregatedStats::GetTotal() const AggregatedVector& AggregatedStats::GetStats() { - if (static_cast(myOptions.DataSourceChoice) == DataSource::Skills) + switch (static_cast(myOptions.DataSourceChoice)) { + case DataSource::Skills: return GetSkills(); - } - else - { + case DataSource::Agents: return GetAgents(); + case DataSource::Totals: + default: + return GetGroupFilterTotals(); } } @@ -340,12 +346,17 @@ const AggregatedVector& AggregatedStats::GetSkillDetails(uint32_t pSkillId) return entry->second; } -TotalHealingStats AggregatedStats::GetTotalHealing() +const AggregatedVector& AggregatedStats::GetGroupFilterTotals() { - TotalHealingStats stats; - for (auto& i : stats) + if (myGroupFilterTotals != nullptr) { - i = 0.0; + return *myGroupFilterTotals; + } + + myGroupFilterTotals = std::make_unique(); + for (uint32_t i = 0; i < static_cast(GroupFilter::Max); i++) + { + myGroupFilterTotals->emplace_back(0, GROUP_FILTER_STRING[i], 0, 0, std::nullopt); } HealWindowOptions fakeOptions; @@ -356,7 +367,7 @@ TotalHealingStats AggregatedStats::GetTotalHealing() // Loop through the array and pretend index is GroupFilter, if agent does not get filtered by that filter then add // the total healing to that agent to the total for that filter - for (size_t i = 0; i < stats.size(); i++) + for (size_t i = 0; i < static_cast(GroupFilter::Max); i++) { switch (static_cast(i)) { @@ -394,18 +405,13 @@ TotalHealingStats AggregatedStats::GetTotalHealing() if (FilterInternal(mapAgent, fakeOptions) == false) { - stats[i] += agent.TotalHealing; + (*myGroupFilterTotals)[i].Healing += agent.TotalHealing; + (*myGroupFilterTotals)[i].Hits += agent.Ticks; } } } - // Divide the total number by time in combat to make it per second instead - for (auto& i : stats) - { - i /= (static_cast(mySourceData.TimeInCombat) / 1000); - } - - return stats; + return *myGroupFilterTotals; } const std::map& AggregatedStats::GetAllAgents() diff --git a/AggregatedStats.h b/AggregatedStats.h index 6489295..ade9296 100644 --- a/AggregatedStats.h +++ b/AggregatedStats.h @@ -33,7 +33,7 @@ struct AggregatedStatsEntry }; using AggregatedVector = std::vector; -using TotalHealingStats = std::array(GroupFilter::Max)>; +using TotalHealingStats = std::array(GroupFilter::Max)>; constexpr static uint32_t IndirectHealingSkillId = 0; @@ -47,6 +47,8 @@ class AggregatedStats const AggregatedVector& GetDetails(uint64_t pId); uint64_t GetCombatTime(); + const AggregatedVector& GetGroupFilterTotals(); + private: const AggregatedVector& GetAgents(); const AggregatedVector& GetSkills(); @@ -54,7 +56,6 @@ class AggregatedStats const AggregatedVector& GetAgentDetails(uintptr_t pAgentId); const AggregatedVector& GetSkillDetails(uint32_t pSkillId); - TotalHealingStats GetTotalHealing(); const std::map& GetAllAgents(); @@ -75,6 +76,7 @@ class AggregatedStats std::unique_ptr myTotal; std::unique_ptr myFilteredAgents; std::unique_ptr mySkills; + std::unique_ptr myGroupFilterTotals; std::map myAgentsDetailed; // uintptr_t => agent id std::map mySkillsDetailed; // uint32_t => skill id diff --git a/GUI.cpp b/GUI.cpp index 9ada564..b67aad6 100644 --- a/GUI.cpp +++ b/GUI.cpp @@ -9,10 +9,6 @@ #include #include -constexpr const char* GROUP_FILTER_STRING[] = { "Group", "Squad", "All (Excluding Summons)", "All (Including Summons)" }; -static_assert((sizeof(GROUP_FILTER_STRING) / sizeof(GROUP_FILTER_STRING[0])) == static_cast(GroupFilter::Max), "Added group filter option without updating gui?"); -constexpr static uint32_t MAX_GROUP_FILTER_NAME = LongestStringInArray(GroupFilter::Max) - 1>::value; - static void Display_DetailsWindow(HealWindowContext& pContext, DetailsWindowState& pState) { if (pState.IsOpen == false) @@ -103,6 +99,70 @@ static void Display_DetailsWindow(HealWindowContext& pContext, DetailsWindowStat ImGui::End(); } +static void Display_Content(HealWindowContext& pContext, uint32_t pWindowIndex) +{ + char buffer[1024]; + + uint64_t timeInCombat = pContext.CurrentAggregatedStats->GetCombatTime(); + const AggregatedStatsEntry& aggregatedTotal = pContext.CurrentAggregatedStats->GetTotal(); + + for (const auto& entry : pContext.CurrentAggregatedStats->GetStats()) + { + ImGui::BeginGroup(); + ImGui::PushID(static_cast(entry.Id)); + float startX = ImGui::GetCursorPosX(); + ImGui::Selectable("", false, ImGuiSelectableFlags_SpanAllColumns); + ImGui::PopID(); + + ImGui::SameLine(); + ImGui::SetCursorPosX(startX); + ImGui::Text("%s", entry.Name.c_str()); + + std::array>, 7> entryValues{ + entry.Healing, + entry.Hits, + entry.Casts, + divide_safe(entry.Healing, timeInCombat), + divide_safe(entry.Healing, entry.Hits), + entry.Casts.has_value() == true ? std::optional{divide_safe(entry.Healing, *entry.Casts)} : std::nullopt, + static_cast(pContext.DataSourceChoice) != DataSource::Totals ? std::optional{divide_safe(entry.Healing * 100, aggregatedTotal.Healing)} : std::nullopt }; + ReplaceFormatted(buffer, sizeof(buffer), pContext.EntryFormat, entryValues); + ImGuiEx::TextRightAlignedSameLine("%s", buffer); + + ImGui::EndGroup(); + + DetailsWindowState* state = nullptr; + if (entry.Id != 0) + { + for (auto& iter : pContext.OpenDetailWindows) + { + if (iter.Id == entry.Id) + { + state = &(iter); + break; + } + } + } + + // If it was opened in a previous frame, we need the update the statistics stored so they are up to date + if (state != nullptr) + { + *static_cast(state) = entry; + } + + if (ImGui::IsItemClicked() == true && entry.Id != 0) + { + if (state == nullptr) + { + state = &pContext.OpenDetailWindows.emplace_back(entry); + } + state->IsOpen = !state->IsOpen; + + LOG("Toggled details window for entry %llu %s in window %u", entry.Id, entry.Name.c_str(), pWindowIndex); + } + } +} + void SetContext(void* pImGuiContext) { ImGui::SetCurrentContext((ImGuiContext*)pImGuiContext); @@ -132,18 +192,28 @@ void Display_GUI(HealTableOptions& pHealingOptions) } uint64_t timeInCombat = curWindow.CurrentAggregatedStats->GetCombatTime(); - const AggregatedStatsEntry& aggregatedTotal = curWindow.CurrentAggregatedStats->GetTotal(); - std::array>, 7> titleValues{ - aggregatedTotal.Healing, - aggregatedTotal.Hits, - aggregatedTotal.Casts, - divide_safe(aggregatedTotal.Healing, timeInCombat), - divide_safe(aggregatedTotal.Healing, aggregatedTotal.Hits), - aggregatedTotal.Casts.has_value() == true ? std::optional{divide_safe(aggregatedTotal.Healing, *aggregatedTotal.Casts)} : std::nullopt, - timeInCombat}; - size_t written = ReplaceFormatted(buffer, 128, curWindow.TitleFormat, titleValues); - snprintf(buffer + written, sizeof(buffer) - written, "###HEALWINDOW%u", i); + + if (static_cast(curWindow.DataSourceChoice) != DataSource::Totals) + { + std::array>, 7> titleValues{ + aggregatedTotal.Healing, + aggregatedTotal.Hits, + aggregatedTotal.Casts, + divide_safe(aggregatedTotal.Healing, timeInCombat), + divide_safe(aggregatedTotal.Healing, aggregatedTotal.Hits), + aggregatedTotal.Casts.has_value() == true ? std::optional{divide_safe(aggregatedTotal.Healing, *aggregatedTotal.Casts)} : std::nullopt, + timeInCombat }; + size_t written = ReplaceFormatted(buffer, 128, curWindow.TitleFormat, titleValues); + snprintf(buffer + written, sizeof(buffer) - written, "###HEALWINDOW%u", i); + } + else + { + std::array>, 7> titleValues{ + timeInCombat }; + size_t written = ReplaceFormatted(buffer, 128, curWindow.TitleFormat, titleValues); + snprintf(buffer + written, sizeof(buffer) - written, "###HEALWINDOW%u", i); + } ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver); ImGui::Begin(buffer, &curWindow.Shown, ImGuiWindowFlags_NoCollapse); @@ -155,43 +225,67 @@ void Display_GUI(HealTableOptions& pHealingOptions) ImGui::Combo("data source", &curWindow.DataSourceChoice, dataSourceItems, static_cast(DataSource::Max)); ImGuiEx::AddTooltipToLastItem("Decides how targets and skills are sorted in the 'Targets' and 'Skills' sections."); - const char* const sortOrderItems[] = {"alphabetical ascending", "alphabetical descending", "heal per second ascending", "heal per second descending"}; - static_assert((sizeof(sortOrderItems) / sizeof(sortOrderItems[0])) == static_cast(SortOrder::Max), "Added sort option without updating gui?"); - ImGui::Combo("sort order", &curWindow.SortOrderChoice, sortOrderItems, static_cast(SortOrder::Max)); - ImGuiEx::AddTooltipToLastItem("Decides how targets and skills are sorted in the 'Targets' and 'Skills' sections."); - - if (ImGui::BeginMenu("stats exclude") == true) + if (static_cast(curWindow.DataSourceChoice) != DataSource::Totals) { - ImGui::Checkbox("group", &curWindow.ExcludeGroup); - ImGui::Checkbox("off-group", &curWindow.ExcludeOffGroup); - ImGui::Checkbox("off-squad", &curWindow.ExcludeOffSquad); - ImGui::Checkbox("summons", &curWindow.ExcludeMinions); - ImGui::Checkbox("unmapped", &curWindow.ExcludeUnmapped); - ImGui::EndMenu(); + const char* const sortOrderItems[] = { "alphabetical ascending", "alphabetical descending", "heal per second ascending", "heal per second descending" }; + static_assert((sizeof(sortOrderItems) / sizeof(sortOrderItems[0])) == static_cast(SortOrder::Max), "Added sort option without updating gui?"); + ImGui::Combo("sort order", &curWindow.SortOrderChoice, sortOrderItems, static_cast(SortOrder::Max)); + ImGuiEx::AddTooltipToLastItem("Decides how targets and skills are sorted in the 'Targets' and 'Skills' sections."); + + if (ImGui::BeginMenu("stats exclude") == true) + { + ImGui::Checkbox("group", &curWindow.ExcludeGroup); + ImGui::Checkbox("off-group", &curWindow.ExcludeOffGroup); + ImGui::Checkbox("off-squad", &curWindow.ExcludeOffSquad); + ImGui::Checkbox("summons", &curWindow.ExcludeMinions); + ImGui::Checkbox("unmapped", &curWindow.ExcludeUnmapped); + ImGui::EndMenu(); + } } ImGui::InputText("short name", curWindow.Name, sizeof(curWindow.Name)); ImGuiEx::AddTooltipToLastItem("The name used to represent this window in the \"heal stats\" menu"); ImGui::InputText("window title", curWindow.TitleFormat, sizeof(curWindow.TitleFormat)); - ImGuiEx::AddTooltipToLastItem("Format for the title of this window.\n" - "{1}: Total healing\n" - "{2}: Total hits\n" - "{3}: Total casts (not implemented yet)\n" - "{4}: Healing per second\n" - "{5}: Healing per hit\n" - "{6}: Healing per cast (not implemented yet)\n" - "{7}: Time in combat\n"); + if (static_cast(curWindow.DataSourceChoice) != DataSource::Totals) + { + ImGuiEx::AddTooltipToLastItem("Format for the title of this window.\n" + "{1}: Total healing\n" + "{2}: Total hits\n" + "{3}: Total casts (not implemented yet)\n" + "{4}: Healing per second\n" + "{5}: Healing per hit\n" + "{6}: Healing per cast (not implemented yet)\n" + "{7}: Time in combat"); + } + else + { + ImGuiEx::AddTooltipToLastItem("Format for the title of this window.\n" + "{1}: Time in combat"); + } ImGui::InputText("entry format", curWindow.EntryFormat, sizeof(curWindow.EntryFormat)); - ImGuiEx::AddTooltipToLastItem("Format for displayed data (statistics are per entry).\n" - "{1}: Healing\n" - "{2}: Hits\n" - "{3}: Casts (not implemented yet)\n" - "{4}: Healing per second\n" - "{5}: Healing per hit\n" - "{6}: Healing per cast (not implemented yet)\n" - "{7}: Percent of total healing\n"); + if (static_cast(curWindow.DataSourceChoice) != DataSource::Totals) + { + ImGuiEx::AddTooltipToLastItem("Format for displayed data (statistics are per entry).\n" + "{1}: Healing\n" + "{2}: Hits\n" + "{3}: Casts (not implemented yet)\n" + "{4}: Healing per second\n" + "{5}: Healing per hit\n" + "{6}: Healing per cast (not implemented yet)\n" + "{7}: Percent of total healing"); + } + else + { + ImGuiEx::AddTooltipToLastItem("Format for displayed data (statistics are per entry).\n" + "{1}: Healing\n" + "{2}: Hits\n" + "{3}: Casts (not implemented yet)\n" + "{4}: Healing per second\n" + "{5}: Healing per hit\n" + "{6}: Healing per cast (not implemented yet)"); + } float oldPosY = ImGui::GetCursorPosY(); ImGui::BeginGroup(); @@ -208,63 +302,12 @@ void Display_GUI(HealTableOptions& pHealingOptions) ImGui::EndGroup(); ImGuiEx::AddTooltipToLastItem("Numerical value (virtual key code) for the key\n" - "used to open and close this window"); + "used to open and close this window"); ImGui::EndPopup(); } - for (const auto& entry : curWindow.CurrentAggregatedStats->GetStats()) - { - ImGui::BeginGroup(); - ImGui::PushID(static_cast(entry.Id)); - float startX = ImGui::GetCursorPosX(); - ImGui::Selectable("", false, ImGuiSelectableFlags_SpanAllColumns); - ImGui::PopID(); - - ImGui::SameLine(); - ImGui::SetCursorPosX(startX); - ImGui::Text("%s", entry.Name.c_str()); - - std::array>, 7> entryValues{ - entry.Healing, - entry.Hits, - entry.Casts, - divide_safe(entry.Healing, timeInCombat), - divide_safe(entry.Healing, entry.Hits), - entry.Casts.has_value() == true ? std::optional{divide_safe(entry.Healing, *entry.Casts)} : std::nullopt, - divide_safe(entry.Healing * 100, aggregatedTotal.Healing)}; - ReplaceFormatted(buffer, sizeof(buffer), curWindow.EntryFormat, entryValues); - ImGuiEx::TextRightAlignedSameLine("%s", buffer); - - ImGui::EndGroup(); - - DetailsWindowState* state = nullptr; - for (auto& iter : curWindow.OpenDetailWindows) - { - if (iter.Id == entry.Id) - { - state = &(iter); - break; - } - } - - // If it was opened in a previous frame, we need the update the statistics stored so they are up to date - if (state != nullptr) - { - *static_cast(state) = entry; - } - - if (ImGui::IsItemClicked() == true) - { - if (state == nullptr) - { - state = &curWindow.OpenDetailWindows.emplace_back(entry); - } - state->IsOpen = !state->IsOpen; - - LOG("Toggled details window for entry %llu %s in window %u", entry.Id, entry.Name.c_str(), i); - } - } + Display_Content(curWindow, i); auto iter = curWindow.OpenDetailWindows.begin(); while (iter != curWindow.OpenDetailWindows.end()) diff --git a/Options.cpp b/Options.cpp index af8cb42..4b13871 100644 --- a/Options.cpp +++ b/Options.cpp @@ -185,23 +185,28 @@ DetailsWindowState::DetailsWindowState(const AggregatedStatsEntry& pEntry) HealTableOptions::HealTableOptions() { - Windows[0].DataSourceChoice = static_cast(DataSource::Agents); - snprintf(Windows[0].Name, sizeof(Windows[0].Name), "%s", "Targets"); - snprintf(Windows[0].TitleFormat, sizeof(Windows[0].TitleFormat), "%s", "Targets {1} ({4}/s, {7}s in combat)"); - - Windows[1].DataSourceChoice = static_cast(DataSource::Skills); - snprintf(Windows[1].Name, sizeof(Windows[1].Name), "%s", "Skills"); - snprintf(Windows[1].TitleFormat, sizeof(Windows[1].TitleFormat), "%s", "Skills {1} ({4}/s, {7}s in combat)"); - - Windows[2].DataSourceChoice = static_cast(DataSource::Agents); - snprintf(Windows[2].Name, sizeof(Windows[2].Name), "%s", "Targets (hits)"); - snprintf(Windows[2].TitleFormat, sizeof(Windows[2].TitleFormat), "%s", "Targets {1} ({5}/hit, {2} hits)"); - snprintf(Windows[2].EntryFormat, sizeof(Windows[2].EntryFormat), "%s", "{1} ({5}/hit, {2} hits)"); - snprintf(Windows[2].DetailsEntryFormat, sizeof(Windows[2].DetailsEntryFormat), "%s", "{1} ({5}/hit, {2} hits)"); - - Windows[3].DataSourceChoice = static_cast(DataSource::Skills); - snprintf(Windows[3].Name, sizeof(Windows[3].Name), "%s", "Skills (hits)"); - snprintf(Windows[3].TitleFormat, sizeof(Windows[3].TitleFormat), "%s", "Skills {1} ({5}/hit, {2} hits)"); + Windows[0].DataSourceChoice = static_cast(DataSource::Totals); + snprintf(Windows[0].Name, sizeof(Windows[0].Name), "%s", "Totals"); + snprintf(Windows[0].TitleFormat, sizeof(Windows[0].TitleFormat), "%s", "Totals ({1}s in combat)"); + snprintf(Windows[0].EntryFormat, sizeof(Windows[0].EntryFormat), "%s", "{1} ({4}/s)"); + + Windows[1].DataSourceChoice = static_cast(DataSource::Agents); + snprintf(Windows[1].Name, sizeof(Windows[1].Name), "%s", "Targets"); + snprintf(Windows[1].TitleFormat, sizeof(Windows[1].TitleFormat), "%s", "Targets {1} ({4}/s, {7}s in combat)"); + + Windows[2].DataSourceChoice = static_cast(DataSource::Skills); + snprintf(Windows[2].Name, sizeof(Windows[2].Name), "%s", "Skills"); + snprintf(Windows[2].TitleFormat, sizeof(Windows[2].TitleFormat), "%s", "Skills {1} ({4}/s, {7}s in combat)"); + + Windows[3].DataSourceChoice = static_cast(DataSource::Agents); + snprintf(Windows[3].Name, sizeof(Windows[3].Name), "%s", "Targets (hits)"); + snprintf(Windows[3].TitleFormat, sizeof(Windows[3].TitleFormat), "%s", "Targets {1} ({5}/hit, {2} hits)"); snprintf(Windows[3].EntryFormat, sizeof(Windows[3].EntryFormat), "%s", "{1} ({5}/hit, {2} hits)"); snprintf(Windows[3].DetailsEntryFormat, sizeof(Windows[3].DetailsEntryFormat), "%s", "{1} ({5}/hit, {2} hits)"); + + Windows[4].DataSourceChoice = static_cast(DataSource::Skills); + snprintf(Windows[4].Name, sizeof(Windows[4].Name), "%s", "Skills (hits)"); + snprintf(Windows[4].TitleFormat, sizeof(Windows[4].TitleFormat), "%s", "Skills {1} ({5}/hit, {2} hits)"); + snprintf(Windows[4].EntryFormat, sizeof(Windows[4].EntryFormat), "%s", "{1} ({5}/hit, {2} hits)"); + snprintf(Windows[4].DetailsEntryFormat, sizeof(Windows[4].DetailsEntryFormat), "%s", "{1} ({5}/hit, {2} hits)"); } diff --git a/State.h b/State.h index dcf3b26..540f8f6 100644 --- a/State.h +++ b/State.h @@ -11,6 +11,7 @@ enum class DataSource { Agents = 0, Skills = 1, + Totals = 2, Max }; diff --git a/arcdps_personal_stats.vcxproj b/arcdps_personal_stats.vcxproj index 0ec5ea3..771376b 100644 --- a/arcdps_personal_stats.vcxproj +++ b/arcdps_personal_stats.vcxproj @@ -190,9 +190,10 @@ + - + \ No newline at end of file