// Lot STM1 (STM1-SPEC-01, T2). Voir LPCityTraceSubsystem.h. #include "LPCityTraceSubsystem.h" #include "LifePlay.h" #if !UE_BUILD_SHIPPING #include "Components/StaticMeshComponent.h" #include "Engine/GameInstance.h" #include "Engine/StaticMesh.h" #include "GameFramework/Actor.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformTime.h" #include "UObject/UnrealType.h" namespace LPCityTrace { // Noms etablis par les dumps des graphes du GI (lots STB, STE, L6f ; STM1-CART-02 ยง 1). const TCHAR* const CommandName = TEXT("LP.CityTrace"); // Valeur d'une propriete en texte lisible : FText et FString tels quels, le reste par ExportText. FString ValueToString(const FProperty* Prop, const void* ValuePtr) { if (!Prop || !ValuePtr) { return FString(); } if (const FTextProperty* TextProp = CastField(Prop)) { return TextProp->GetPropertyValue(ValuePtr).ToString(); } if (const FStrProperty* StrProp = CastField(Prop)) { return StrProp->GetPropertyValue(ValuePtr); } if (const FNameProperty* NameProp = CastField(Prop)) { return NameProp->GetPropertyValue(ValuePtr).ToString(); } if (const FObjectPropertyBase* ObjectProp = CastField(Prop)) { const UObject* Object = ObjectProp->GetObjectPropertyValue(ValuePtr); return Object ? Object->GetPathName() : FString(TEXT("None")); } FString Out; Prop->ExportTextItem_Direct(Out, ValuePtr, nullptr, nullptr, PPF_None); return Out; } // Membre d'une structure Blueprint : son nom porte un suffixe de GUID (DisplayName_2_EBDEAF...), cherche par prefixe. const FProperty* FindMemberByPrefix(const UStruct* Struct, const TCHAR* Prefix) { if (!Struct) { return nullptr; } for (TFieldIterator It(Struct); It; ++It) { if (It->GetName().StartsWith(Prefix, ESearchCase::IgnoreCase)) { return *It; } } return nullptr; } FString Member(const UStruct* Struct, const void* StructPtr, const TCHAR* Prefix) { const FProperty* Prop = FindMemberByPrefix(Struct, Prefix); return Prop ? ValueToString(Prop, Prop->ContainerPtrToValuePtr(StructPtr)) : FString(TEXT("?")); } // Une ville (structure City) en une ligne : nom affiche, donnees StreetMap, maillage. FString DescribeCity(const UStruct* Struct, const void* StructPtr) { return FString::Printf(TEXT("'%s' sm='%s' mesh='%s'"), *Member(Struct, StructPtr, TEXT("DisplayName_")), *Member(Struct, StructPtr, TEXT("StreetMapName_")), *Member(Struct, StructPtr, TEXT("StaticMeshName_"))); } } void ULPCityTraceSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); StartSeconds = FPlatformTime::Seconds(); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPCityTraceSubsystem::Tick), 0.25f); // Une instance de jeu par PIE : la commande n'est enregistree que par la premiere. if (!IConsoleManager::Get().FindConsoleObject(LPCityTrace::CommandName)) { ConsoleCommand = IConsoleManager::Get().RegisterConsoleCommand(LPCityTrace::CommandName, TEXT("LifePlay (STM1) : ecrit au journal l'etat du choix de la ville ([LPCity])."), FConsoleCommandDelegate::CreateUObject(this, &ULPCityTraceSubsystem::DumpNow), ECVF_Default); } UE_LOG(LogLPCity, Log, TEXT("[LPCity] trace active (releve toutes les 0,25 s, ecrit au changement)")); } void ULPCityTraceSubsystem::Deinitialize() { if (TickHandle.IsValid()) { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); TickHandle.Reset(); } if (ConsoleCommand) { IConsoleManager::Get().UnregisterConsoleObject(ConsoleCommand); ConsoleCommand = nullptr; } Super::Deinitialize(); } bool ULPCityTraceSubsystem::Tick(float DeltaTime) { Sample(false); return true; } void ULPCityTraceSubsystem::DumpNow() { Sample(true); } void ULPCityTraceSubsystem::Sample(bool bForce) { const UGameInstance* GI = GetGameInstance(); if (!GI) { return; } const UClass* GIClass = GI->GetClass(); auto Missing = [this](const UObject* Owner, const TCHAR* Name) { const FString Key = FString::Printf(TEXT("%s.%s"), Owner ? *Owner->GetClass()->GetName() : TEXT("?"), Name); bool bAlreadyReported = false; ReportedMissing.Add(Key, &bAlreadyReported); if (!bAlreadyReported) { UE_LOG(LogLPCity, Log, TEXT("[LPCity] PROPRIETE ABSENTE %s"), *Key); } }; // T2a : liste des villes (GI.Cities, tableau de City). const FArrayProperty* CitiesProp = CastField(GIClass->FindPropertyByName(TEXT("Cities"))); const FStructProperty* CityInner = CitiesProp ? CastField(CitiesProp->Inner) : nullptr; if (!CityInner) { Missing(GI, TEXT("Cities")); } else { FScriptArrayHelper Cities(CitiesProp, CitiesProp->ContainerPtrToValuePtr(GI)); const int32 Count = Cities.Num(); if (Count > 0 && (bForce || Count != LastCityCount)) { LastCityCount = Count; int32 LondonIndex = INDEX_NONE; for (int32 Index = 0; Index < Count; ++Index) { if (LPCityTrace::Member(CityInner->Struct, Cities.GetRawPtr(Index), TEXT("StreetMapName_")).Contains(TEXT("/CentralLondon"))) { LondonIndex = Index; break; } } const FString LondonText = LondonIndex == INDEX_NONE ? FString(TEXT("absent")) : FString::FromInt(LondonIndex); UE_LOG(LogLPCity, Log, TEXT("[LPCity] CITIES n=%d [0]=%s ; CentralLondon a l'indice %s"), Count, *LPCityTrace::DescribeCity(CityInner->Struct, Cities.GetRawPtr(0)), *LondonText); } } // T2b : ville sauvegardee (GI.SaveGameInstance.City) et correspondance, comme GI.GetCityByName : // nom affiche (DisplayName) compare sans tenir compte de la casse. const FObjectPropertyBase* SaveProp = CastField(GIClass->FindPropertyByName(TEXT("SaveGameInstance"))); const UObject* Save = SaveProp ? SaveProp->GetObjectPropertyValue_InContainer(GI) : nullptr; if (!SaveProp) { Missing(GI, TEXT("SaveGameInstance")); } else if (Save) { const FProperty* SaveCityProp = Save->GetClass()->FindPropertyByName(TEXT("City")); if (!SaveCityProp) { Missing(Save, TEXT("City")); } else { const FString SavedCity = LPCityTrace::ValueToString(SaveCityProp, SaveCityProp->ContainerPtrToValuePtr(Save)); FString Match = TEXT("liste des villes illisible"); if (CityInner) { Match = TEXT("AUCUNE correspondance (GetCityByName rendra la City par defaut)"); FScriptArrayHelper Cities(CitiesProp, CitiesProp->ContainerPtrToValuePtr(GI)); for (int32 Index = 0; Index < Cities.Num(); ++Index) { const FString DisplayName = LPCityTrace::Member(CityInner->Struct, Cities.GetRawPtr(Index), TEXT("DisplayName_")); if (DisplayName.Equals(SavedCity, ESearchCase::IgnoreCase)) { Match = FString::Printf(TEXT("trouvee indice %d ('%s')"), Index, *DisplayName); break; } } } const FString Line = FString::Printf(TEXT("ville='%s' -> %s"), *SavedCity, *Match); if (bForce || Line != LastSave) { LastSave = Line; UE_LOG(LogLPCity, Log, TEXT("[LPCity] SAUVEGARDE %s"), *Line); } } } // T2c : ville demandee (GI.CityToMoveTo). const FStructProperty* RequestProp = CastField(GIClass->FindPropertyByName(TEXT("CityToMoveTo"))); if (!RequestProp) { Missing(GI, TEXT("CityToMoveTo")); } else { const FString Line = LPCityTrace::DescribeCity(RequestProp->Struct, RequestProp->ContainerPtrToValuePtr(GI)); if (bForce || Line != LastRequest) { LastRequest = Line; UE_LOG(LogLPCity, Log, TEXT("[LPCity] DEMANDE %s"), *Line); } } // T2d et T2e : donnees StreetMap et maillage poses sur l'acteur de la carte (GI.StreetMapStatic). const FObjectPropertyBase* StreetProp = CastField(GIClass->FindPropertyByName(TEXT("StreetMapStatic"))); const AActor* Street = StreetProp ? Cast(StreetProp->GetObjectPropertyValue_InContainer(GI)) : nullptr; if (!StreetProp) { Missing(GI, TEXT("StreetMapStatic")); } if (!Street) { return; } const FObjectPropertyBase* MapProp = CastField(Street->GetClass()->FindPropertyByName(TEXT("StreetMap"))); if (!MapProp) { Missing(Street, TEXT("StreetMap")); } else { const UObject* Map = MapProp->GetObjectPropertyValue_InContainer(Street); const FString Line = Map ? Map->GetPathName() : FString(TEXT("None")); if (bForce || Line != LastStreetMap) { LastStreetMap = Line; UE_LOG(LogLPCity, Log, TEXT("[LPCity] STREETMAP %s"), *Line); } } const FObjectPropertyBase* MeshCompProp = CastField(Street->GetClass()->FindPropertyByName(TEXT("StaticMeshComponent"))); if (!MeshCompProp) { Missing(Street, TEXT("StaticMeshComponent")); } else { const UStaticMeshComponent* MeshComp = Cast(MeshCompProp->GetObjectPropertyValue_InContainer(Street)); const UStaticMesh* Mesh = MeshComp ? MeshComp->GetStaticMesh() : nullptr; const FString Line = Mesh ? Mesh->GetPathName() : FString(TEXT("None")); if (bForce || Line != LastPose) { LastPose = Line; UE_LOG(LogLPCity, Log, TEXT("[LPCity] POSE %s (t=%.1f s)"), *Line, FPlatformTime::Seconds() - StartSeconds); } } } #else void ULPCityTraceSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); } void ULPCityTraceSubsystem::Deinitialize() { Super::Deinitialize(); } bool ULPCityTraceSubsystem::Tick(float DeltaTime) { return false; } void ULPCityTraceSubsystem::Sample(bool bForce) { } void ULPCityTraceSubsystem::DumpNow() { } #endif