From f7aa8eeb962bf39415a4c01472fde0d8100a476a Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Thu, 30 Jul 2026 17:08:25 +0800 Subject: [PATCH 1/7] Submit --- YRpp | 2 +- src/Spawner/Spawner.Config.cpp | 1 + src/Spawner/Spawner.Config.h | 2 + src/Spawner/Statistics.cpp | 92 +++++++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 2 deletions(-) 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..3d417ee0 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,10 @@ #include #include +#include +#include +#include + bool __forceinline IsStatisticsEnabled() { return Spawner::Active @@ -35,7 +40,84 @@ bool __forceinline IsStatisticsEnabled() && !SessionClass::IsCampaign(); } -// Write stats.dmp +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; + + using GetUnitCountFunc = int(__fastcall*)(void*); + const auto GetUnitCount = reinterpret_cast(0x49FB60); + + const int count = HouseClass::Array.Count; + + for (int i = 0; i < count; i++) + { + const auto pHouse = HouseClass::Array.GetItem(i); + if (!pHouse) + continue; + + if (pHouse->Type && pHouse->Type->MultiplayPassive) + continue; + + if (pHouse->IsInitiallyObserver()) + continue; + + char name[64] = { 0 }; + if (pHouse->IsHumanPlayer) + { + WideCharToMultiByte(CP_UTF8, 0, pHouse->UIName, -1, name, sizeof(name), nullptr, nullptr); + } + else + { + strcpy_s(name, "Computer"); + } + + const char* result = pHouse->Defeated ? "Loser" : "Winner"; + + int lost = *reinterpret_cast( + reinterpret_cast(pHouse) + 21556) + + *reinterpret_cast( + reinterpret_cast(pHouse) + 21640); + + const int* k1 = reinterpret_cast( + reinterpret_cast(pHouse) + 21476); + const int* k2 = reinterpret_cast( + reinterpret_cast(pHouse) + 21560); + int kills = 0; + for (int j = 0; j < 20; ++j) kills += k1[j]; + for (int j = 0; j < 20; ++j) kills += k2[j]; + + int built = GetUnitCount(reinterpret_cast(pHouse) + 21920) + + GetUnitCount(reinterpret_cast(pHouse) + 21940) + + GetUnitCount(reinterpret_cast(pHouse) + 21960) + + GetUnitCount(reinterpret_cast(pHouse) + 21980); + + int score = *reinterpret_cast( + reinterpret_cast(pHouse) + 21736); + + 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))); + } + + 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(); +} + DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) { if (IsStatisticsEnabled()) @@ -50,6 +132,8 @@ DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) statsFile.Close(); } + WriteDTALog(); + bool& bStatisticsPacketSent = *reinterpret_cast(0xA8F900); bStatisticsPacketSent = true; @@ -59,6 +143,12 @@ DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) return 0; } +DEFINE_HOOK(0x5C9D47, MPScore_InitDialog_WriteDTALog, 0x6) +{ + WriteDTALog(); + return 0; +} + // Send AI player // Dont send observer DEFINE_HOOK(0x6C73F8, SendStatisticsPacket_HouseFilter, 0x6) From cf7f92bbabb41483763939dd7170f6c9898d2f8b Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Thu, 30 Jul 2026 22:45:05 +0800 Subject: [PATCH 2/7] Change and correct --- src/Spawner/Statistics.cpp | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index 3d417ee0..a7d15f7c 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -54,9 +54,6 @@ static void WriteDTALog() if (!file.Open(FileAccessMode::Write)) return; - using GetUnitCountFunc = int(__fastcall*)(void*); - const auto GetUnitCount = reinterpret_cast(0x49FB60); - const int count = HouseClass::Array.Count; for (int i = 0; i < count; i++) @@ -83,26 +80,19 @@ static void WriteDTALog() const char* result = pHouse->Defeated ? "Loser" : "Winner"; - int lost = *reinterpret_cast( - reinterpret_cast(pHouse) + 21556) - + *reinterpret_cast( - reinterpret_cast(pHouse) + 21640); - const int* k1 = reinterpret_cast( - reinterpret_cast(pHouse) + 21476); - const int* k2 = reinterpret_cast( - reinterpret_cast(pHouse) + 21560); + int lost = pHouse->TotalKilledUnits + pHouse->TotalKilledBuildings; + int kills = 0; - for (int j = 0; j < 20; ++j) kills += k1[j]; - for (int j = 0; j < 20; ++j) kills += k2[j]; + for (int j = 0; j < 20; ++j) kills += pHouse->KilledUnitsOfHouses[j]; + for (int j = 0; j < 20; ++j) kills += pHouse->KilledBuildingsOfHouses[j]; - int built = GetUnitCount(reinterpret_cast(pHouse) + 21920) - + GetUnitCount(reinterpret_cast(pHouse) + 21940) - + GetUnitCount(reinterpret_cast(pHouse) + 21960) - + GetUnitCount(reinterpret_cast(pHouse) + 21980); + int built = pHouse->FactoryProducedBuildingTypes.GetTotal() + + pHouse->FactoryProducedUnitTypes.GetTotal() + + pHouse->FactoryProducedInfantryTypes.GetTotal() + + pHouse->FactoryProducedAircraftTypes.GetTotal(); - int score = *reinterpret_cast( - reinterpret_cast(pHouse) + 21736); + int score = pHouse->PointTotal; char buffer[256] = { 0 }; sprintf_s(buffer, "%s: %s\n Lost = %d\n Kills = %d\n Built = %d\n Score = %d\n", From ca018a1da651c8431795e2561869b5bdc712f471 Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Thu, 30 Jul 2026 22:53:09 +0800 Subject: [PATCH 3/7] Remove unnecessary changes --- src/Spawner/Statistics.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index a7d15f7c..cba295f9 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -29,10 +29,6 @@ #include #include -#include -#include -#include - bool __forceinline IsStatisticsEnabled() { return Spawner::Active From 42a57770d848256a37346279fd49ce1073b28512 Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Thu, 30 Jul 2026 23:26:23 +0800 Subject: [PATCH 4/7] making changes and improvements --- src/Spawner/Statistics.cpp | 72 +++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index cba295f9..a6b25015 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -29,6 +29,31 @@ #include #include + +// MP score global statistics array, populated by the original sub_5C98A0 +// during MPScore_InitDialog. The in-game score screen reads from the same +// array, so reading it here guarantees DTA.LOG matches the in-game display +// exactly (including Score's if(>0) filter and the winner bonus). +// Each record is 112 bytes with 16-byte field spacing (Westwood-style align). +struct MPScoreStatsEntry +{ + wchar_t Name[20]; // 0x00 Player name (copy of pHouse->UIName) + int Scheme; // 0x28 Scenario index + int Winner; // 0x2C 1=winner 0=loser + int Lost; // 0x30 Lost = TotalKilledUnits + TotalKilledBuildings + int _pad0[3]; // 0x34 + int Kills; // 0x40 Kills = sum(KilledUnitsOfHouses) + sum(KilledBuildingsOfHouses) + int _pad1[3]; // 0x44 + int Built; // 0x50 Built = sum of 4 FactoryProduced*Types.GetTotal() + int _pad2[3]; // 0x54 + int Score; // 0x60 Score (with if(>0) filter and winner bonus applied) + int _pad3[3]; // 0x64 +}; +static_assert(sizeof(MPScoreStatsEntry) == 112, "MPScoreStatsEntry size mismatch"); + +DEFINE_POINTER(MPScoreStatsEntry, MPScoreStatsArray, 0xA8D1FCu) +DEFINE_POINTER(int, MPScoreStatsCount, 0xA8D580u) + bool __forceinline IsStatisticsEnabled() { return Spawner::Active @@ -50,49 +75,23 @@ static void WriteDTALog() if (!file.Open(FileAccessMode::Write)) return; - const int count = HouseClass::Array.Count; + // Read from the global 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 = *MPScoreStatsCount; for (int i = 0; i < count; i++) { - const auto pHouse = HouseClass::Array.GetItem(i); - if (!pHouse) - continue; - - if (pHouse->Type && pHouse->Type->MultiplayPassive) - continue; - - if (pHouse->IsInitiallyObserver()) - continue; + const auto& entry = MPScoreStatsArray[i]; char name[64] = { 0 }; - if (pHouse->IsHumanPlayer) - { - WideCharToMultiByte(CP_UTF8, 0, pHouse->UIName, -1, name, sizeof(name), nullptr, nullptr); - } - else - { - strcpy_s(name, "Computer"); - } - - const char* result = pHouse->Defeated ? "Loser" : "Winner"; - - - int lost = pHouse->TotalKilledUnits + pHouse->TotalKilledBuildings; + WideCharToMultiByte(CP_UTF8, 0, entry.Name, -1, name, sizeof(name), nullptr, nullptr); - int kills = 0; - for (int j = 0; j < 20; ++j) kills += pHouse->KilledUnitsOfHouses[j]; - for (int j = 0; j < 20; ++j) kills += pHouse->KilledBuildingsOfHouses[j]; - - int built = pHouse->FactoryProducedBuildingTypes.GetTotal() - + pHouse->FactoryProducedUnitTypes.GetTotal() - + pHouse->FactoryProducedInfantryTypes.GetTotal() - + pHouse->FactoryProducedAircraftTypes.GetTotal(); - - int score = pHouse->PointTotal; + const char* result = entry.Winner ? "Winner" : "Loser"; 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); + name, result, entry.Lost, entry.Kills, entry.Built, entry.Score); file.WriteBytes(buffer, static_cast(strlen(buffer))); } @@ -118,8 +117,6 @@ DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) statsFile.Close(); } - WriteDTALog(); - bool& bStatisticsPacketSent = *reinterpret_cast(0xA8F900); bStatisticsPacketSent = true; @@ -131,6 +128,9 @@ DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) 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; } From 3296534a0f301b6ce8930078674a7e18434221be Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Thu, 30 Jul 2026 23:32:50 +0800 Subject: [PATCH 5/7] Add back what was accidentally deleted --- src/Spawner/Statistics.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index a6b25015..b9676510 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -103,6 +103,7 @@ static void WriteDTALog() file.Close(); } +// Write stats.dmp DEFINE_HOOK(0x6C856C, SendStatisticsPacket_WriteStatisticsDump, 0x5) { if (IsStatisticsEnabled()) From b241a349669379726b5079b11b23f8b1febcef34 Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Fri, 31 Jul 2026 00:11:38 +0800 Subject: [PATCH 6/7] Submit the new changes --- src/Spawner/Statistics.cpp | 44 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index b9676510..302220fe 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -30,29 +30,10 @@ #include -// MP score global statistics array, populated by the original sub_5C98A0 -// during MPScore_InitDialog. The in-game score screen reads from the same -// array, so reading it here guarantees DTA.LOG matches the in-game display -// exactly (including Score's if(>0) filter and the winner bonus). -// Each record is 112 bytes with 16-byte field spacing (Westwood-style align). -struct MPScoreStatsEntry -{ - wchar_t Name[20]; // 0x00 Player name (copy of pHouse->UIName) - int Scheme; // 0x28 Scenario index - int Winner; // 0x2C 1=winner 0=loser - int Lost; // 0x30 Lost = TotalKilledUnits + TotalKilledBuildings - int _pad0[3]; // 0x34 - int Kills; // 0x40 Kills = sum(KilledUnitsOfHouses) + sum(KilledBuildingsOfHouses) - int _pad1[3]; // 0x44 - int Built; // 0x50 Built = sum of 4 FactoryProduced*Types.GetTotal() - int _pad2[3]; // 0x54 - int Score; // 0x60 Score (with if(>0) filter and winner bonus applied) - int _pad3[3]; // 0x64 -}; -static_assert(sizeof(MPScoreStatsEntry) == 112, "MPScoreStatsEntry size mismatch"); - -DEFINE_POINTER(MPScoreStatsEntry, MPScoreStatsArray, 0xA8D1FCu) -DEFINE_POINTER(int, MPScoreStatsCount, 0xA8D580u) +// 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() { @@ -75,23 +56,28 @@ static void WriteDTALog() if (!file.Open(FileAccessMode::Write)) return; - // Read from the global array populated by sub_5C98A0, the same data - // source as the in-game score screen. The array is filled after + // 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 = *MPScoreStatsCount; + const int count = SessionClass::MPScoreCount; for (int i = 0; i < count; i++) { - const auto& entry = MPScoreStatsArray[i]; + const auto& entry = SessionClass::MPScores[i]; char name[64] = { 0 }; WideCharToMultiByte(CP_UTF8, 0, entry.Name, -1, name, sizeof(name), nullptr, nullptr); - const char* result = entry.Winner ? "Winner" : "Loser"; + 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, entry.Lost, entry.Kills, entry.Built, entry.Score); + name, result, lost, kills, built, score); file.WriteBytes(buffer, static_cast(strlen(buffer))); } From e2fae37bcf4141bc58dc9a83b58a10237847e7b3 Mon Sep 17 00:00:00 2001 From: GreyVacuum <2168314900@qq.com> Date: Fri, 31 Jul 2026 00:23:51 +0800 Subject: [PATCH 7/7] Fix the corresponding problem --- src/Spawner/Statistics.cpp | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Spawner/Statistics.cpp b/src/Spawner/Statistics.cpp index 302220fe..26f08ff9 100644 --- a/src/Spawner/Statistics.cpp +++ b/src/Spawner/Statistics.cpp @@ -61,12 +61,30 @@ static void WriteDTALog() // MPScore_InitDialog (0x5C9D42) calls sub_5C98A0. const int count = SessionClass::MPScoreCount; - for (int i = 0; i < count; i++) + // 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) { - const auto& entry = SessionClass::MPScores[i]; + // 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, entry.Name, -1, name, sizeof(name), nullptr, nullptr); + WideCharToMultiByte(CP_UTF8, 0, display_name, -1, name, sizeof(name), nullptr, nullptr); const char* result = entry.Wins ? "Winner" : "Loser"; @@ -79,6 +97,8 @@ static void WriteDTALog() 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 };