diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index becf193..43356e5 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -299,3 +299,5 @@ Build `D:\Game\LifePlay_Build_5.5_L5a` (instrumentée) : éditeur et jeu compil Suite : session 4.27 jouant les mêmes scènes de quête, puis comparaison normalisée. Note de traçabilité : la sauvegarde `L5-instr` de `LPSessionLogSubsystem.*` a été prise après modification ; la version antérieure est dans git (`0baf4cf`). + +| CHG-028 | 2026-09-29 | L5 | `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` | capteur d'exceptions de script (-LPTraceScriptEx, -LPTraceLocals, -LPTraceMax) : relève les variables locales du cadre Blueprint au moment de l'erreur ; actif seulement avec l'option (le moteur supprime sa pile d'appel quand un écouteur est branché : elle est réécrite). Validé en -game. | `Tools\Restore.ps1 -Lot L5-trace` | diff --git a/Source/LifePlay/LPSessionLogSubsystem.cpp b/Source/LifePlay/LPSessionLogSubsystem.cpp index b1ecb38..bfae0a1 100644 --- a/Source/LifePlay/LPSessionLogSubsystem.cpp +++ b/Source/LifePlay/LPSessionLogSubsystem.cpp @@ -11,6 +11,9 @@ #include "Misc/CommandLine.h" #include "Misc/Parse.h" #include "UObject/UnrealType.h" +#include "UObject/Script.h" +#include "UObject/Stack.h" +#include "Blueprint/BlueprintExceptionInfo.h" #include "Misc/EngineVersion.h" #include "Misc/OutputDevice.h" #include "Misc/Paths.h" @@ -129,6 +132,70 @@ void ULPSessionLogSubsystem::Initialize(FSubsystemCollectionBase& Collection) LogStartupSummary(); DumpRequestedVariables(TEXT("demarrage")); + StartScriptExceptionTrace(); +} + +void ULPSessionLogSubsystem::StartScriptExceptionTrace() +{ + FString Filter; + if (!FParse::Value(FCommandLine::Get(), TEXT("LPTraceScriptEx="), Filter, false) || Filter.IsEmpty()) + { + return; + } + FString LocalsList; + FParse::Value(FCommandLine::Get(), TEXT("LPTraceLocals="), LocalsList, false); + TArray Keywords; + LocalsList.ParseIntoArray(Keywords, TEXT(","), true); + int32 Max = 3; + FParse::Value(FCommandLine::Get(), TEXT("LPTraceMax="), Max); + + UE_LOG(LogLifePlay, Display, TEXT("Trace des exceptions de script : filtre '%s', variables [%s], %d releve(s) au plus"), *Filter, *LocalsList, Max); + + TSharedRef Count = MakeShared(0); + ScriptExceptionHandle = FBlueprintCoreDelegates::OnScriptException.AddLambda( + [Filter, Keywords, Max, Count](const UObject* ActiveObject, const FFrame& StackFrame, const FBlueprintExceptionInfo& Info) + { + // Brancher un ecouteur supprime la pile d'appel que le moteur ecrit d'ordinaire : + // on la reecrit pour toutes les exceptions, pas seulement celles relevees. + const FString StackTrace = StackFrame.GetStackTrace(); + UE_LOG(LogScript, Warning, TEXT("%s"), *StackTrace); + + const FString Description = Info.GetDescription().ToString(); + if (*Count >= Max || !Description.Contains(Filter) || !StackFrame.Node || !StackFrame.Locals) + { + return; + } + ++*Count; + + UE_LOG(LogLifePlay, Warning, TEXT("[trace %d/%d] %s | objet %s | fonction %s | offset %d"), + *Count, Max, *Description, *GetNameSafe(ActiveObject), *StackFrame.Node->GetName(), + StackFrame.Code && StackFrame.Node->Script.Num() ? (int32)(StackFrame.Code - StackFrame.Node->Script.GetData()) : -1); + + int32 Shown = 0; + for (TFieldIterator It(StackFrame.Node); It && Shown < 200; ++It) + { + const FProperty* Prop = *It; + FString TypeName = Prop->GetCPPType(); + if (const FStructProperty* SP = CastField(Prop)) { TypeName = SP->Struct->GetName(); } + if (const FArrayProperty* AP = CastField(Prop)) + { + if (const FStructProperty* ISP = CastField(AP->Inner)) { TypeName = TEXT("TArray<") + ISP->Struct->GetName() + TEXT(">"); } + } + + const FString PropName = Prop->GetName(); + const bool bMatch = Keywords.ContainsByPredicate([&](const FString& K) + { return PropName.Contains(K) || TypeName.Contains(K); }); + if (!bMatch) + { + continue; + } + + FString Value; + Prop->ExportTextItem_Direct(Value, Prop->ContainerPtrToValuePtr(StackFrame.Locals), nullptr, nullptr, PPF_None); + UE_LOG(LogLifePlay, Warning, TEXT("[trace %d] %s (%s) = %s"), *Count, *PropName, *TypeName, *Value.Left(500)); + ++Shown; + } + }); } void ULPSessionLogSubsystem::DumpRequestedVariables(const TCHAR* When) const @@ -178,6 +245,11 @@ void ULPSessionLogSubsystem::DumpRequestedVariables(const TCHAR* When) const void ULPSessionLogSubsystem::Deinitialize() { DumpRequestedVariables(TEXT("fin")); + if (ScriptExceptionHandle.IsValid()) + { + FBlueprintCoreDelegates::OnScriptException.Remove(ScriptExceptionHandle); + ScriptExceptionHandle.Reset(); + } LogSessionSummary(); if (Counter.IsValid()) diff --git a/Source/LifePlay/LPSessionLogSubsystem.h b/Source/LifePlay/LPSessionLogSubsystem.h index 592d633..8c6e2d4 100644 --- a/Source/LifePlay/LPSessionLogSubsystem.h +++ b/Source/LifePlay/LPSessionLogSubsystem.h @@ -38,6 +38,17 @@ private: */ void DumpRequestedVariables(const TCHAR* When) const; + /** + * Diagnostic (lot L5) : avec -LPTraceScriptEx= -LPTraceLocals=, a chaque + * exception de script Blueprint dont la description contient (3 premieres + * seulement, -LPTraceMax=N pour changer), ecrit la pile d'appel et les variables locales + * du cadre dont le nom ou le type de structure contient l'un des mots. + * Sans l'option, rien n'est branche : le moteur garde son comportement de journal normal. + */ + void StartScriptExceptionTrace(); + + FDelegateHandle ScriptExceptionHandle; + TSharedPtr Counter; double StartTime = 0.0; };