From d6d2c569c7104eea00fb7739def925adad18d3c7 Mon Sep 17 00:00:00 2001 From: edujiro5972 <336072401+edujiro5972@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:51:35 +0200 Subject: [PATCH] L7i : AI vide via AIDummy (scripts .lpai), Quick dress (ERR-037) ; outils LPDumpSave et releve sur motif du journal Co-Authored-By: Claude Opus 5.5 --- Docs/Migration/Journal_Modifications.md | 19 +++ Docs/Migration/Registre_Erreurs.md | 1 + Source/LifePlay/LPSessionLogSubsystem.cpp | 128 ++++++++++++++++++ Source/LifePlay/LPSessionLogSubsystem.h | 12 ++ .../LPDumpSaveCommandlet.cpp | 94 +++++++++++++ .../LPDumpSaveCommandlet.h | 24 ++++ 6 files changed, 278 insertions(+) create mode 100644 Source/LifePlayEditorTools/LPDumpSaveCommandlet.cpp create mode 100644 Source/LifePlayEditorTools/LPDumpSaveCommandlet.h diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index c28ed07..98421aa 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -610,3 +610,22 @@ la partie, pas seulement au chargement. Build de diagnostic `LifePlay_Build_5.5_L7i` (contenu identique à L7b, 8561 `.uasset`), lancement par `LifePlay_diagnostic_L7i.bat` (300 relevés, 4 cadres appelants, `-ScriptStackOnWarnings`). + +Session capteur n° 1 (`L7i_partie2_20261001.log`, 53 min, 300 relevés) : les variables vides viennent surtout des +scripts d'IA des PNJ (`.lpai` : `meal`, `church`, `grocery`, `entertainment`…), via `UpdateAI → PlayScene → +ReadSceneLine → CallFunction → getActorVar/setActorVar`. La variable vide est **`AI`**, alimentée par `GI.AIDummy` +(personnage factice du pool `DummyPersons`, attribué par `SpawnDummy`). `GI.PlayerCharacter` n'est pas None. Dans +`GetVariable`, toutes les variables spéciales (`Player`, `Ptr`, `Interacting`, `CurrentCompanion`, `A1`…`A5`) +passent par le même `MakeStruct` dont le nom est figé à « Player » : l'étiquette relevée en L6a était trompeuse. + +Nouveau défaut signalé (utilisateur, 2026-10-01) : **Quick dress laisse souvent le personnage principal nu ; correct +en 4.27** → ERR-037, régression UE5. Chemin : `Action` (bouton « Quick dress ») → `GI::Dress(PersonVariable="Player", +ClothVariables=[], QuickDress=true)` → pour chaque emplacement obligatoire (`Top`, `Bottom`, `Foot`), filtre de +l'inventaire du joueur (sexe, emplacement, `Overlaps`, `Daz == (Race == "Daz")`, exclusion `Wedding/Nurse/Doctor`). +Inventaire de la sauvegarde `1.sav` cohérent (80 vêtements, `Daz=True`). + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +|---|---|---|---|---|---| +| CHG-041 | 2026-10-01 | L7i | `Source/LifePlayEditorTools/LPDumpSaveCommandlet.h/.cpp` (nouveau : contenu d'un `.sav` en texte, lecture seule) ; `Source/LifePlay/LPSessionLogSubsystem.h/.cpp` : relevé déclenché par un motif du journal (`-LPDumpOnLog`, `-LPDumpPaths`, `-LPDumpMax`), inactif sans l'option | ERR-037 : relever l'état du joueur au moment du Quick dress ; ERR-003 : état d'`AIDummy` | `git checkout L7i-capteur -- Source` | + +Build `LifePlay_Build_5.5_L7i` reconstruite (contenu identique), lancement `LifePlay_diagnostic_L7i_2.bat`. diff --git a/Docs/Migration/Registre_Erreurs.md b/Docs/Migration/Registre_Erreurs.md index cab8739..59f8f90 100644 --- a/Docs/Migration/Registre_Erreurs.md +++ b/Docs/Migration/Registre_Erreurs.md @@ -41,3 +41,4 @@ | ERR-034 | La barre de temps est décalée vers la droite | retour utilisateur 2026-09-30 | — | faible (visuel) | L6e | **corrigé (CHG-033, CX-B09)** — cause : marge par défaut des `UButton` passée de 2 px (UE4 `FCoreStyle`) à 12 px horizontaux (UE5 `FUMGCoreStyle`) ; 5 boutons × 20 px = 100 px de débordement ; **validé en jeu le 2026-09-30** (build L6e, confirmé par l'utilisateur) | | ERR-035 | 6 nœuds `GetRandomPointInNavigableRadius` dépréciés (R-04 : `Person` 3, `Room` 2, `AI` 1) | `LogBlueprint` | 6 (compilation), 3 (cuisson) | faible (dette) | L7b | **corrigé (CHG-037, CX-B11)**, retiré à tort (CHG-038), réappliqué (CHG-039) : compilation et cuisson 0 avertissement ; **validé en jeu le 2026-09-30** (build L7b) | | ERR-036 | En extérieur (ex. `forest___vin01.lpworld`), personnages et objets (bois du feu de camp) enfoncés sous la surface du sol ; ils traversent la matière | retour utilisateur 2026-09-30 (capture) | — | faible (visuel) | L7l | **préexistant en 4.27** (confirmé par l'utilisateur), présent aussi en V1/L7a : hors périmètre de la migration ; amélioration possible si souhaitée | +| ERR-037 | Quick dress (menu Action) laisse souvent le personnage principal nu | retour utilisateur 2026-10-01 | — | moyenne (visible) | L7i | **ouvert**, régression UE5 (correct en 4.27 selon l'utilisateur) ; capteur de relevé en place (CHG-041) | diff --git a/Source/LifePlay/LPSessionLogSubsystem.cpp b/Source/LifePlay/LPSessionLogSubsystem.cpp index 8ff1670..0649eb4 100644 --- a/Source/LifePlay/LPSessionLogSubsystem.cpp +++ b/Source/LifePlay/LPSessionLogSubsystem.cpp @@ -18,6 +18,7 @@ #include "Misc/OutputDevice.h" #include "Misc/Paths.h" #include "Misc/ScopeLock.h" +#include "Containers/Ticker.h" namespace { @@ -113,6 +114,48 @@ private: int32 Overflow = 0; }; +/** Repere un motif dans le journal et planifie un releve sur le fil de jeu. N'ecrit jamais dans le journal. */ +class FLPLogTrigger : public FOutputDevice +{ +public: + FLPLogTrigger(const FString& InPattern, int32 InMax, TFunction InOnMatch) + : Pattern(InPattern), Max(InMax), OnMatch(MoveTemp(InOnMatch)) {} + + virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override + { + if (Category == LogLifePlay.GetCategoryName() || !FCString::Strstr(V, *Pattern)) + { + return; + } + FScopeLock Lock(&Mutex); + const double Now = FPlatformTime::Seconds(); + if (Count >= Max || Now - Last < 5.0) + { + return; + } + Last = Now; + const int32 Index = ++Count; + const FString Line(V); + // Releve differe : le Blueprint a le temps de terminer l'habillage, et on quitte le journal. + FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([OnMatch = OnMatch, Index, Line](float) + { + OnMatch(Index, Line); + return false; + }), 1.5f); + } + + virtual bool CanBeUsedOnAnyThread() const override { return true; } + virtual bool CanBeUsedOnMultipleThreads() const override { return true; } + +private: + FCriticalSection Mutex; + FString Pattern; + int32 Max = 20; + int32 Count = 0; + double Last = -1000.0; + TFunction OnMatch; +}; + void ULPSessionLogSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); @@ -133,6 +176,7 @@ void ULPSessionLogSubsystem::Initialize(FSubsystemCollectionBase& Collection) LogStartupSummary(); DumpRequestedVariables(TEXT("demarrage")); StartScriptExceptionTrace(); + StartLogTriggeredDump(); } namespace @@ -300,8 +344,92 @@ void ULPSessionLogSubsystem::DumpRequestedVariables(const TCHAR* When) const } } +void ULPSessionLogSubsystem::StartLogTriggeredDump() +{ + FString Pattern; + if (!FParse::Value(FCommandLine::Get(), TEXT("LPDumpOnLog="), Pattern, false) || Pattern.IsEmpty()) + { + return; + } + FString Paths; + FParse::Value(FCommandLine::Get(), TEXT("LPDumpPaths="), Paths, false); + Paths.ParseIntoArray(DumpPathList, TEXT(","), true); + int32 Max = 20; + FParse::Value(FCommandLine::Get(), TEXT("LPDumpMax="), Max); + + UE_LOG(LogLifePlay, Display, TEXT("Releve sur motif du journal : '%s', chemins [%s], %d releve(s) au plus"), *Pattern, *Paths, Max); + + TWeakObjectPtr WeakThis(this); + Trigger = MakeShared(Pattern, Max, [WeakThis](int32 Index, const FString& Line) + { + if (const ULPSessionLogSubsystem* Self = WeakThis.Get()) + { + Self->DumpPaths(Index, Line); + } + }); + GLog->AddOutputDevice(Trigger.Get()); +} + +void ULPSessionLogSubsystem::DumpPaths(int32 Index, const FString& Line) const +{ + UE_LOG(LogLifePlay, Warning, TEXT("[releve %d] declencheur : %s"), Index, *Line.Left(300)); + UObject* Root = GetGameInstance(); + for (const FString& Path : DumpPathList) + { + TArray Parts; + Path.ParseIntoArray(Parts, TEXT("."), true); + const UObject* Obj = Root; + FString Value = TEXT(""); + for (int32 i = 0; i < Parts.Num() && Obj; ++i) + { + const FProperty* Prop = Obj->GetClass()->FindPropertyByName(FName(*Parts[i])); + if (!Prop) + { + Value = FString::Printf(TEXT("<%s introuvable sur %s>"), *Parts[i], *Obj->GetClass()->GetName()); + break; + } + const void* Ptr = Prop->ContainerPtrToValuePtr(Obj); + if (i + 1 < Parts.Num()) + { + const FObjectPropertyBase* ObjProp = CastField(Prop); + Obj = ObjProp ? ObjProp->GetObjectPropertyValue(Ptr) : nullptr; + if (!Obj) + { + Value = FString::Printf(TEXT("<%s vaut None>"), *Parts[i]); + } + continue; + } + Value.Reset(); + if (const FArrayProperty* ArrayProp = CastField(Prop)) + { + FScriptArrayHelper Helper(ArrayProp, Ptr); + UE_LOG(LogLifePlay, Warning, TEXT("[releve %d] %s : %d element(s)"), Index, *Path, Helper.Num()); + for (int32 j = 0; j < Helper.Num() && j < 120; ++j) + { + FString Item; + ArrayProp->Inner->ExportTextItem_Direct(Item, Helper.GetRawPtr(j), nullptr, const_cast(Obj), PPF_None); + UE_LOG(LogLifePlay, Warning, TEXT("[releve %d] %s[%d] = %s"), Index, *Path, j, *Item.Left(700)); + } + Value = TEXT(""); + break; + } + Prop->ExportTextItem_Direct(Value, Ptr, nullptr, const_cast(Obj), PPF_None); + } + if (!Value.IsEmpty()) + { + UE_LOG(LogLifePlay, Warning, TEXT("[releve %d] %s = %s"), Index, *Path, *Value.Left(700)); + } + } +} + void ULPSessionLogSubsystem::Deinitialize() { + if (Trigger.IsValid()) + { + GLog->RemoveOutputDevice(Trigger.Get()); + GLog->Flush(); + Trigger.Reset(); + } DumpRequestedVariables(TEXT("fin")); if (ScriptExceptionHandle.IsValid()) { diff --git a/Source/LifePlay/LPSessionLogSubsystem.h b/Source/LifePlay/LPSessionLogSubsystem.h index 7b7796e..8af0d08 100644 --- a/Source/LifePlay/LPSessionLogSubsystem.h +++ b/Source/LifePlay/LPSessionLogSubsystem.h @@ -50,8 +50,20 @@ private: */ void StartScriptExceptionTrace(); + /** + * Diagnostic (lot L7i) : avec -LPDumpOnLog=, chaque fois qu'une ligne du journal contient + * (au plus une fois toutes les 5 s, -LPDumpMax=N releves, 20 par defaut), ecrit 1,5 s plus + * tard les proprietes nommees par -LPDumpPaths= a partir de l'instance de jeu (GI) : un + * chemin "PlayerCharacter.Race" suit la propriete objet PlayerCharacter puis lit Race. + * Sans l'option, rien n'est branche. + */ + void StartLogTriggeredDump(); + void DumpPaths(int32 Index, const FString& Line) const; + FDelegateHandle ScriptExceptionHandle; TSharedPtr Counter; + TSharedPtr Trigger; + TArray DumpPathList; double StartTime = 0.0; }; diff --git a/Source/LifePlayEditorTools/LPDumpSaveCommandlet.cpp b/Source/LifePlayEditorTools/LPDumpSaveCommandlet.cpp new file mode 100644 index 0000000..2080abc --- /dev/null +++ b/Source/LifePlayEditorTools/LPDumpSaveCommandlet.cpp @@ -0,0 +1,94 @@ +// Portage UE 5.5 - voir LPDumpSaveCommandlet.h + +#include "LPDumpSaveCommandlet.h" + +#include "GameFramework/SaveGame.h" +#include "Kismet/GameplayStatics.h" +#include "Misc/FileHelper.h" +#include "UObject/UnrealType.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPDumpSave, Log, All); + +ULPDumpSaveCommandlet::ULPDumpSaveCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPDumpSaveCommandlet::Main(const FString& Params) +{ + TArray Tokens, Switches; + TMap ParamsMap; + ParseCommandLine(*Params, Tokens, Switches, ParamsMap); + + const FString File = ParamsMap.FindRef(TEXT("File")); + const FString Out = ParamsMap.FindRef(TEXT("Out")); + TArray Wanted; + ParamsMap.FindRef(TEXT("Props")).ParseIntoArray(Wanted, TEXT(","), true); + if (File.IsEmpty() || Out.IsEmpty()) + { + UE_LOG(LogLPDumpSave, Error, TEXT("Usage : -File= -Out= [-Props=Nom1,Nom2]")); + return 1; + } + + TArray Bytes; + if (!FFileHelper::LoadFileToArray(Bytes, *File)) + { + UE_LOG(LogLPDumpSave, Error, TEXT("Lecture impossible : %s"), *File); + return 1; + } + USaveGame* Save = UGameplayStatics::LoadGameFromMemory(Bytes); + if (!Save) + { + UE_LOG(LogLPDumpSave, Error, TEXT("Sauvegarde illisible : %s"), *File); + return 1; + } + + TArray Lines; + Lines.Add(FString::Printf(TEXT("##### %s [%s]"), *File, *Save->GetClass()->GetPathName())); + for (TFieldIterator It(Save->GetClass()); It; ++It) + { + const FProperty* Prop = *It; + if (Prop->GetOwnerClass() && !Prop->GetOwnerClass()->IsChildOf(USaveGame::StaticClass())) + { + continue; + } + if (Prop->GetOwnerClass() == USaveGame::StaticClass()) + { + continue; + } + const FString Name = Prop->GetName(); + if (Wanted.Num() > 0 && !Wanted.ContainsByPredicate([&](const FString& W) { return Name.StartsWith(W); })) + { + continue; + } + const void* Value = Prop->ContainerPtrToValuePtr(Save); + if (const FArrayProperty* ArrayProp = CastField(Prop)) + { + FScriptArrayHelper Helper(ArrayProp, Value); + Lines.Add(FString::Printf(TEXT("%s [%s] : %d element(s)"), *Name, *ArrayProp->Inner->GetCPPType(), Helper.Num())); + for (int32 i = 0; i < Helper.Num(); ++i) + { + FString Text; + ArrayProp->Inner->ExportTextItem_Direct(Text, Helper.GetRawPtr(i), nullptr, Save, PPF_None); + Lines.Add(FString::Printf(TEXT(" %s[%d] = %s"), *Name, i, *Text)); + } + } + else + { + FString Text; + Prop->ExportTextItem_Direct(Text, Value, nullptr, Save, PPF_None); + Lines.Add(FString::Printf(TEXT("%s [%s] = %s"), *Name, *Prop->GetCPPType(), *Text)); + } + } + + if (!FFileHelper::SaveStringArrayToFile(Lines, *Out, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogLPDumpSave, Error, TEXT("Ecriture impossible : %s"), *Out); + return 1; + } + UE_LOG(LogLPDumpSave, Warning, TEXT("=== %s : %d ligne(s) -> %s ==="), *File, Lines.Num(), *Out); + return 0; +} diff --git a/Source/LifePlayEditorTools/LPDumpSaveCommandlet.h b/Source/LifePlayEditorTools/LPDumpSaveCommandlet.h new file mode 100644 index 0000000..662f975 --- /dev/null +++ b/Source/LifePlayEditorTools/LPDumpSaveCommandlet.h @@ -0,0 +1,24 @@ +// Portage UE 5.5 - lot L7i (diagnostic Quick dress). +// +// Ecrit en texte le contenu d'une sauvegarde (.sav) : chaque propriete de l'objet USaveGame +// (classe Blueprint du projet), un element de tableau par ligne. Lecture seule : le fichier +// n'est jamais reecrit. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPDumpSave -File= -Out= [-Props=Nom1,Nom2] + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPDumpSaveCommandlet.generated.h" + +UCLASS() +class ULPDumpSaveCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPDumpSaveCommandlet(); + virtual int32 Main(const FString& Params) override; +};