LifePlay-Revival/Source/LifePlay/LPSessionLogSubsystem.cpp
edujiro5972 58016141e8 L5 : option de diagnostic -LPDumpVars (contenu des variables de GI au demarrage et a la fin)
Dump de MeshSexRace en -game : 24 corps, Mesh vide au demarrage pour tous
(chargement a la demande), 6 corps legacy jamais charges (LegacyPercentage=0).
Presets de quete : race Daz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:50:00 +02:00

269 lines
8.8 KiB
C++

// Portage UE 5.5 - lot L3 (journalisation). Voir LPSessionLogSubsystem.h.
#include "LPSessionLogSubsystem.h"
#include "LifePlay.h"
#include "GeneralProjectSettings.h"
#include "HAL/FileManager.h"
#include "HAL/IConsoleManager.h"
#include "Engine/GameInstance.h"
#include "Misc/App.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include "UObject/UnrealType.h"
#include "Misc/EngineVersion.h"
#include "Misc/OutputDevice.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
namespace
{
// Au-dela, les nouvelles signatures sont comptees ensemble : la memoire reste bornee
// meme si un Blueprint emet des messages tous differents pendant des heures.
constexpr int32 MaxSignatures = 4000;
constexpr int32 SignatureLength = 240;
constexpr int32 TopSignatures = 25;
// Remplace chaque suite de chiffres par '#' : "Person_C_2147482340" et
// "index 12 ... length 12" se regroupent sous une seule signature. Les sauts de ligne
// deviennent " | " : la pile d'appel Blueprint ("Script call stack:") reste lisible.
FString MakeSignature(const TCHAR* Message)
{
FString Out;
Out.Reserve(SignatureLength);
bool bInDigits = false;
for (const TCHAR* C = Message; *C && Out.Len() < SignatureLength; ++C)
{
if (*C == TEXT('\r') || *C == TEXT('\n') || *C == TEXT('\t'))
{
if (!Out.EndsWith(TEXT(" | ")) && !Out.IsEmpty()) { Out += TEXT(" | "); }
bInDigits = false;
}
else if (FChar::IsDigit(*C))
{
if (!bInDigits) { Out.AppendChar(TEXT('#')); }
bInDigits = true;
}
else
{
Out.AppendChar(*C);
bInDigits = false;
}
}
return Out;
}
// Une seule commande console pour le processus, portee par la premiere instance de jeu
// (en PIE plusieurs GameInstances peuvent coexister).
FAutoConsoleCommand* SummaryCommand = nullptr;
const void* SummaryCommandOwner = nullptr;
}
/** Compte les messages du journal. N'ecrit jamais dans le journal lui-meme. */
class FLPLogCounter : public FOutputDevice
{
public:
virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override
{
const ELogVerbosity::Type Level = (ELogVerbosity::Type)(Verbosity & ELogVerbosity::VerbosityMask);
if (Category == LogLifePlay.GetCategoryName())
{
return;
}
FScopeLock Lock(&Mutex);
++PerCategory.FindOrAdd(FString::Printf(TEXT("%s %s"), *Category.ToString(), ToString(Level)));
if (Level <= ELogVerbosity::Warning)
{
const FString Key = FString::Printf(TEXT("[%s %s] %s"), *Category.ToString(), ToString(Level), *MakeSignature(V));
if (int32* Count = Signatures.Find(Key))
{
++*Count;
}
else if (Signatures.Num() < MaxSignatures)
{
Signatures.Add(Key, 1);
}
else
{
++Overflow;
}
}
}
virtual bool CanBeUsedOnAnyThread() const override { return true; }
virtual bool CanBeUsedOnMultipleThreads() const override { return true; }
void Snapshot(TArray<TPair<FString, int32>>& OutCategories, TArray<TPair<FString, int32>>& OutSignatures, int32& OutOverflow) const
{
FScopeLock Lock(&Mutex);
for (const auto& It : PerCategory) { OutCategories.Emplace(It.Key, It.Value); }
for (const auto& It : Signatures) { OutSignatures.Emplace(It.Key, It.Value); }
OutOverflow = Overflow;
}
private:
mutable FCriticalSection Mutex;
TMap<FString, int32> PerCategory;
TMap<FString, int32> Signatures;
int32 Overflow = 0;
};
void ULPSessionLogSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
StartTime = FPlatformTime::Seconds();
Counter = MakeShared<FLPLogCounter>();
GLog->AddOutputDevice(Counter.Get());
if (!SummaryCommand)
{
SummaryCommand = new FAutoConsoleCommand(
TEXT("LifePlay.LogSummary"),
TEXT("Ecrit dans le journal le bilan de session LifePlay (messages par categorie, signatures les plus frequentes)."),
FConsoleCommandDelegate::CreateWeakLambda(this, [this]() { LogSessionSummary(); }));
SummaryCommandOwner = this;
}
LogStartupSummary();
DumpRequestedVariables(TEXT("demarrage"));
}
void ULPSessionLogSubsystem::DumpRequestedVariables(const TCHAR* When) const
{
FString List;
if (!FParse::Value(FCommandLine::Get(), TEXT("LPDumpVars="), List, false))
{
return;
}
const UGameInstance* GI = GetGameInstance();
if (!GI)
{
return;
}
TArray<FString> Names;
List.ParseIntoArray(Names, TEXT(","), true);
for (const FString& Name : Names)
{
const FProperty* Prop = GI->GetClass()->FindPropertyByName(FName(*Name));
if (!Prop)
{
UE_LOG(LogLifePlay, Warning, TEXT("[dump %s] %s : variable introuvable sur %s"), When, *Name, *GI->GetClass()->GetName());
continue;
}
if (const FArrayProperty* ArrayProp = CastField<FArrayProperty>(Prop))
{
FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr<void>(GI));
UE_LOG(LogLifePlay, Display, TEXT("[dump %s] %s : %d element(s)"), When, *Name, Helper.Num());
for (int32 i = 0; i < Helper.Num(); ++i)
{
FString Text;
ArrayProp->Inner->ExportTextItem_Direct(Text, Helper.GetRawPtr(i), nullptr, const_cast<UGameInstance*>(GI), PPF_None);
UE_LOG(LogLifePlay, Display, TEXT("[dump %s] %s[%d] = %s"), When, *Name, i, *Text.Left(600));
}
}
else
{
FString Text;
Prop->ExportTextItem_Direct(Text, Prop->ContainerPtrToValuePtr<void>(GI), nullptr, const_cast<UGameInstance*>(GI), PPF_None);
UE_LOG(LogLifePlay, Display, TEXT("[dump %s] %s = %s"), When, *Name, *Text.Left(600));
}
}
}
void ULPSessionLogSubsystem::Deinitialize()
{
DumpRequestedVariables(TEXT("fin"));
LogSessionSummary();
if (Counter.IsValid())
{
GLog->RemoveOutputDevice(Counter.Get());
// Un autre thread peut encore etre dans Serialize : on vide la file avant liberation.
GLog->Flush();
Counter.Reset();
}
if (SummaryCommandOwner == this)
{
delete SummaryCommand;
SummaryCommand = nullptr;
SummaryCommandOwner = nullptr;
}
Super::Deinitialize();
}
void ULPSessionLogSubsystem::LogStartupSummary() const
{
const UGeneralProjectSettings* Project = GetDefault<UGeneralProjectSettings>();
UE_LOG(LogLifePlay, Display, TEXT("===== LifePlay : demarrage ====="));
UE_LOG(LogLifePlay, Display, TEXT("Jeu %s %s | moteur %s | configuration %s"),
*Project->ProjectName, *Project->ProjectVersion,
*FEngineVersion::Current().ToString(), LexToString(FApp::GetBuildConfiguration()));
// Meme racine que ModdingExtra::LoadTxt : ProjectContentDir()/Modules
const FString ModulesDir = FPaths::Combine(FPaths::ProjectContentDir(), TEXT("Modules"));
IFileManager& FM = IFileManager::Get();
if (!FM.DirectoryExists(*ModulesDir))
{
UE_LOG(LogLifePlay, Error, TEXT("Dossier de donnees absent : %s (verifier DirectoriesToAlwaysStageAsNonUFS)"),
*FPaths::ConvertRelativePathToFull(ModulesDir));
return;
}
TArray<FString> ModFiles;
FM.FindFilesRecursive(ModFiles, *ModulesDir, TEXT("*.lpmod"), true, false);
ModFiles.Sort();
UE_LOG(LogLifePlay, Display, TEXT("Donnees : %s | %d module(s) | ModLauncher.config %s"),
*FPaths::ConvertRelativePathToFull(ModulesDir), ModFiles.Num(),
FM.FileExists(*FPaths::Combine(ModulesDir, TEXT("ModLauncher.config"))) ? TEXT("present") : TEXT("absent (tous les modules par defaut)"));
for (const FString& ModFile : ModFiles)
{
const FString ModDir = FPaths::GetPath(ModFile);
TArray<FString> Files;
FM.FindFilesRecursive(Files, *ModDir, TEXT("*.*"), true, false);
TMap<FString, int32> PerExt;
for (const FString& F : Files) { ++PerExt.FindOrAdd(FPaths::GetExtension(F).ToLower()); }
PerExt.ValueSort([](int32 A, int32 B) { return A > B; });
FString Detail;
for (const auto& It : PerExt) { Detail += FString::Printf(TEXT(" %s=%d"), *It.Key, It.Value); }
UE_LOG(LogLifePlay, Display, TEXT(" %-22s %5d fichier(s) :%s"), *FPaths::GetBaseFilename(ModFile), Files.Num(), *Detail);
}
}
void ULPSessionLogSubsystem::LogSessionSummary() const
{
if (!Counter.IsValid())
{
return;
}
TArray<TPair<FString, int32>> Categories, Signatures;
int32 Overflow = 0;
Counter->Snapshot(Categories, Signatures, Overflow);
const auto ByCountDesc = [](const TPair<FString, int32>& A, const TPair<FString, int32>& B) { return A.Value > B.Value; };
Categories.Sort(ByCountDesc);
Signatures.Sort(ByCountDesc);
UE_LOG(LogLifePlay, Display, TEXT("===== LifePlay : bilan de session (%.0f s) ====="), FPlatformTime::Seconds() - StartTime);
UE_LOG(LogLifePlay, Display, TEXT("Messages par categorie et verbosite (20 premieres) :"));
for (int32 i = 0; i < FMath::Min(20, Categories.Num()); ++i)
{
UE_LOG(LogLifePlay, Display, TEXT(" %8d %s"), Categories[i].Value, *Categories[i].Key);
}
UE_LOG(LogLifePlay, Display, TEXT("Avertissements et erreurs : %d signature(s) distincte(s)%s ; les %d plus frequentes :"),
Signatures.Num(), Overflow ? *FString::Printf(TEXT(", %d message(s) hors plafond"), Overflow) : TEXT(""), TopSignatures);
for (int32 i = 0; i < FMath::Min(TopSignatures, Signatures.Num()); ++i)
{
UE_LOG(LogLifePlay, Display, TEXT(" %8d %s"), Signatures[i].Value, *Signatures[i].Key);
}
}