diff --git a/YRpp b/YRpp index 762d1aa5..fe5ef7e0 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 762d1aa59c4a27d3a396255d637be495931c98ee +Subproject commit fe5ef7e0e6f54fd38cd586f12322557d542a4a43 diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index 205eca9d..6ccea91b 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -114,6 +114,7 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI) QuickMatch = pINI->ReadBool(pSettingsSection, "QuickMatch", QuickMatch); SkipScoreScreen = pINI->ReadBool(pSettingsSection, "SkipScoreScreen", SkipScoreScreen); WriteStatistics = pINI->ReadBool(pSettingsSection, "WriteStatistics", WriteStatistics); + GenerateStatistics = pINI->ReadBool(pSettingsSection, "GenerateStatistics", GenerateStatistics); AINamesByDifficulty = pINI->ReadBool(pSettingsSection, "AINamesByDifficulty", AINamesByDifficulty); ContinueWithoutHumans = pINI->ReadBool(pSettingsSection, "ContinueWithoutHumans", ContinueWithoutHumans); DefeatedBecomesObserver = pINI->ReadBool(pSettingsSection, "DefeatedBecomesObserver", DefeatedBecomesObserver); diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h index fca7b2be..9da95317 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -143,6 +143,7 @@ class SpawnerConfig bool QuickMatch; bool SkipScoreScreen; bool WriteStatistics; + bool GenerateStatistics; bool AINamesByDifficulty; bool ContinueWithoutHumans; bool DefeatedBecomesObserver; @@ -241,6 +242,7 @@ class SpawnerConfig , QuickMatch { false } , SkipScoreScreen { Main::GetConfig()->SkipScoreScreen } , WriteStatistics { false } + , GenerateStatistics { true } , AINamesByDifficulty { false } , ContinueWithoutHumans { false } , DefeatedBecomesObserver { false } diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index 8d9b49de..26f08ff9 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -20,6 +20,7 @@ #include "Spawner.h" #include +#include #include #include #include @@ -28,6 +29,12 @@ #include #include + +// MAX_MULTI_GAMES index used by the single-result MP score screen. +// The array supports multi-game (tournament) accumulation, but in RA2/YR's +// standard non-tournament MPScore dialog sub_5C98A0 only writes slot 0. +static constexpr int ScoreGameSlot = 0; + bool __forceinline IsStatisticsEnabled() { return Spawner::Active @@ -35,6 +42,73 @@ bool __forceinline IsStatisticsEnabled() && !SessionClass::IsCampaign(); } +static void WriteDTALog() +{ + if (!Spawner::Active) + return; + + if (!Spawner::GetConfig()->GenerateStatistics) + return; + + CreateDirectoryA("debug", nullptr); + + CCFileClass file = CCFileClass("debug\\Statistic.log"); + if (!file.Open(FileAccessMode::Write)) + return; + + // Read from the SessionClass::MPScores array populated by sub_5C98A0, the + // same data source as the in-game score screen. The array is filled after + // MPScore_InitDialog (0x5C9D42) calls sub_5C98A0. + const int count = SessionClass::MPScoreCount; + + // Walk HouseClass::Array in the same order as sub_5C98A0 to map each + // MPScores entry back to its HouseClass, so we can tell human players + // from AI players. AI players are always named "Computer" regardless of + // the localised UIName the game copies into the score array. + int scoreIndex = 0; + + for (auto pHouse : HouseClass::Array) + { + // Same filter as sub_5C98A0: skip null, passive, and observer houses. + if (!pHouse + || (pHouse->Type && pHouse->Type->MultiplayPassive) + || pHouse->IsObserver()) + continue; + + if (scoreIndex >= count) + break; + + const auto& entry = SessionClass::MPScores[scoreIndex]; + + const wchar_t* display_name = pHouse->IsHumanPlayer + ? entry.Name : L"Computer"; + + char name[64] = { 0 }; + WideCharToMultiByte(CP_UTF8, 0, display_name, -1, name, sizeof(name), nullptr, nullptr); + + const char* result = entry.Wins ? "Winner" : "Loser"; + + const int lost = entry.Lost[ScoreGameSlot]; + const int kills = entry.Kills[ScoreGameSlot]; + const int built = entry.Built[ScoreGameSlot]; + const int score = entry.Score[ScoreGameSlot]; + + char buffer[256] = { 0 }; + sprintf_s(buffer, "%s: %s\n Lost = %d\n Kills = %d\n Built = %d\n Score = %d\n", + name, result, lost, kills, built, score); + file.WriteBytes(buffer, static_cast(strlen(buffer))); + + ++scoreIndex; + } + + char fpsBuffer[128] = { 0 }; + sprintf_s(fpsBuffer, "Game loop finished. Average FPS = %d\n", + static_cast(FPSCounter::GetAverageFrameRate())); + file.WriteBytes(fpsBuffer, static_cast(strlen(fpsBuffer))); + + file.Close(); +} + // Write stats.dmp DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) { @@ -59,6 +133,15 @@ DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) return 0; } +DEFINE_HOOK(0x5C9D47, MPScore_InitDialog_WriteDTALog, 0x6) +{ + // sub_5C98A0 has just finished (called at 0x5C9D42), so the global + // statistics array is now populated with the exact same Score shown + // in-game (including the if(>0) filter and the winner bonus). + WriteDTALog(); + return 0; +} + // Send AI player // Dont send observer DEFINE_HOOK(0x6C73F8, SendStatisticsPacket_HouseFilter, 0x6)