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 <noreply@anthropic.com>
This commit is contained in:
parent
0376fb7861
commit
d6d2c569c7
6 changed files with 278 additions and 0 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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<void(int32, const FString&)> 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<void(int32, const FString&)> 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<ULPSessionLogSubsystem> WeakThis(this);
|
||||
Trigger = MakeShared<FLPLogTrigger>(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<FString> Parts;
|
||||
Path.ParseIntoArray(Parts, TEXT("."), true);
|
||||
const UObject* Obj = Root;
|
||||
FString Value = TEXT("<introuvable>");
|
||||
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<void>(Obj);
|
||||
if (i + 1 < Parts.Num())
|
||||
{
|
||||
const FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(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<FArrayProperty>(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<UObject*>(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<UObject*>(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())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,8 +50,20 @@ private:
|
|||
*/
|
||||
void StartScriptExceptionTrace();
|
||||
|
||||
/**
|
||||
* Diagnostic (lot L7i) : avec -LPDumpOnLog=<texte>, chaque fois qu'une ligne du journal contient
|
||||
* <texte> (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,b.c> 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<FLPLogCounter> Counter;
|
||||
TSharedPtr<class FLPLogTrigger> Trigger;
|
||||
TArray<FString> DumpPathList;
|
||||
double StartTime = 0.0;
|
||||
};
|
||||
|
|
|
|||
94
Source/LifePlayEditorTools/LPDumpSaveCommandlet.cpp
Normal file
94
Source/LifePlayEditorTools/LPDumpSaveCommandlet.cpp
Normal file
|
|
@ -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<FString> Tokens, Switches;
|
||||
TMap<FString, FString> ParamsMap;
|
||||
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
|
||||
|
||||
const FString File = ParamsMap.FindRef(TEXT("File"));
|
||||
const FString Out = ParamsMap.FindRef(TEXT("Out"));
|
||||
TArray<FString> Wanted;
|
||||
ParamsMap.FindRef(TEXT("Props")).ParseIntoArray(Wanted, TEXT(","), true);
|
||||
if (File.IsEmpty() || Out.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogLPDumpSave, Error, TEXT("Usage : -File=<chemin.sav> -Out=<fichier.txt> [-Props=Nom1,Nom2]"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
TArray<uint8> 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<FString> Lines;
|
||||
Lines.Add(FString::Printf(TEXT("##### %s [%s]"), *File, *Save->GetClass()->GetPathName()));
|
||||
for (TFieldIterator<FProperty> 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<void>(Save);
|
||||
if (const FArrayProperty* ArrayProp = CastField<FArrayProperty>(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;
|
||||
}
|
||||
24
Source/LifePlayEditorTools/LPDumpSaveCommandlet.h
Normal file
24
Source/LifePlayEditorTools/LPDumpSaveCommandlet.h
Normal file
|
|
@ -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 <projet> -run=LPDumpSave -File=<chemin.sav> -Out=<fichier.txt> [-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;
|
||||
};
|
||||
Loading…
Reference in a new issue