// Portage UE 5.5 - voir LPModInfoSubsystem.h #include "LPModInfoSubsystem.h" #include "Engine/Engine.h" #include "Engine/GameInstance.h" #include "Engine/GameViewportClient.h" #include "GeneralProjectSettings.h" #include "HAL/FileManager.h" #include "HAL/PlatformTime.h" #include "LifePlay.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Styling/CoreStyle.h" #include "UObject/UnrealType.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/Text/STextBlock.h" namespace { constexpr double CheckInterval = 2.0; FString FieldText(const UStruct* Struct, const void* Data, const TCHAR* AuthoredName) { for (TFieldIterator It(Struct); It; ++It) { if (It->GetAuthoredName() == AuthoredName) { FString Out; It->ExportTextItem_InContainer(Out, Data, nullptr, nullptr, PPF_None); return Out.TrimQuotes(); } } return FString(); } } void ULPModInfoSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); GameVersion = GetDefault()->ProjectVersion; // Modules//.lpmod (meme regle que le jeu : un fichier par dossier de module, a sa racine). const FString ModulesDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir() / TEXT("Modules")); TArray Dirs; IFileManager::Get().FindFiles(Dirs, *(ModulesDir / TEXT("*")), false, true); int32 WithGameVersion = 0; for (const FString& Dir : Dirs) { FString Text; if (!FFileHelper::LoadFileToString(Text, *(ModulesDir / Dir / Dir + TEXT(".lpmod")))) { continue; } TMap& Keys = Metadata.Add(Dir); TArray Lines; Text.ParseIntoArrayLines(Lines, true); for (const FString& Line : Lines) { FString Key, Value; if (!Line.TrimStart().StartsWith(TEXT("//")) && Line.Split(TEXT(":"), &Key, &Value)) { Keys.Add(Key.TrimStartAndEnd().ToUpper(), Value.TrimStartAndEnd()); } } WithGameVersion += Keys.Contains(TEXT("MODULE_GAME_VERSION")) ? 1 : 0; } UE_LOG(LogLifePlay, Display, TEXT("Modules : %d .lpmod lu(s), %d avec MODULE_GAME_VERSION (jeu %s)"), Metadata.Num(), WithGameVersion, *GameVersion); if (WithGameVersion > 0) { TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPModInfoSubsystem::Tick)); } } void ULPModInfoSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); CloseWarning(); Super::Deinitialize(); } FString ULPModInfoSubsystem::GetModuleMetadata(const FString& Module, const FString& Key) const { for (const TPair>& Pair : Metadata) { if (Pair.Key.Equals(Module, ESearchCase::IgnoreCase)) { const FString* Value = Pair.Value.Find(Key.ToUpper()); return Value ? *Value : FString(); } } return FString(); } bool ULPModInfoSubsystem::IsModuleEnabled(const FString& Module) const { // Meme regle que GI.IsModEnabled : un module absent de GI.ModEnableds est inactif. const UObject* GI = GetGameInstance(); const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("ModEnableds"))) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner) { return false; } FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 i = 0; i < Helper.Num(); ++i) { if (FieldText(Inner->Struct, Helper.GetRawPtr(i), TEXT("UniqueID")).Equals(Module, ESearchCase::IgnoreCase)) { return FieldText(Inner->Struct, Helper.GetRawPtr(i), TEXT("IsEnabled")).Equals(TEXT("True"), ESearchCase::IgnoreCase); } } return false; } bool ULPModInfoSubsystem::Tick(float DeltaTime) { const double Now = FPlatformTime::Seconds(); if (Now < NextCheck || !GEngine || !GEngine->GameViewport || Popup.IsValid()) { return true; } NextCheck = Now + CheckInterval; TArray Lines; for (const TPair>& Pair : Metadata) { const FString* Built = Pair.Value.Find(TEXT("MODULE_GAME_VERSION")); if (!Built || Built->IsEmpty() || *Built == GameVersion || Warned.Contains(Pair.Key) || !IsModuleEnabled(Pair.Key)) { continue; } Warned.Add(Pair.Key); const FString* Name = Pair.Value.Find(TEXT("MODULE_NAME")); Lines.Add(FString::Printf(TEXT("- %s (%s) : built for LifePlay %s"), Name ? **Name : *Pair.Key, *Pair.Key, **Built)); UE_LOG(LogLifePlay, Warning, TEXT("Modules : %s construit pour LifePlay %s, jeu %s"), *Pair.Key, **Built, *GameVersion); } if (Lines.Num() > 0) { ShowWarning(Lines); } return true; } void ULPModInfoSubsystem::ShowWarning(const TArray& Lines) { const FString Body = FString::Printf(TEXT("This game is LifePlay %s. The following enabled mod(s) were built for another version:\n\n%s\n\n") TEXT("Their 3D assets may fail to load (missing hair, clothes...) or look wrong. Update the mod, or ask its author for a ") TEXT("version built for LifePlay %s. You can disable it in the Mod Manager."), *GameVersion, *FString::Join(Lines, TEXT("\n")), *GameVersion); Popup = SNew(SOverlay) + SOverlay::Slot().HAlign(HAlign_Center).VAlign(VAlign_Center) [ SNew(SBox).WidthOverride(760.f) [ SNew(SBorder) .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) .BorderBackgroundColor(FLinearColor(0.02f, 0.02f, 0.03f, 0.94f)) .Padding(24.f) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 12.f) [ SNew(STextBlock) .Text(FText::FromString(TEXT("Mod version warning"))) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 22)) .ColorAndOpacity(FLinearColor(1.f, 0.75f, 0.2f)) ] + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 18.f) [ SNew(STextBlock) .Text(FText::FromString(Body)) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 14)) .ColorAndOpacity(FLinearColor(0.92f, 0.92f, 0.92f)) .AutoWrapText(true) ] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Right) [ SNew(SButton) .ContentPadding(FMargin(28.f, 6.f)) .OnClicked_Lambda([this]() { CloseWarning(); return FReply::Handled(); }) [ SNew(STextBlock).Text(FText::FromString(TEXT("OK"))).Font(FCoreStyle::GetDefaultFontStyle("Bold", 16)) ] ] ] ] ]; GEngine->GameViewport->AddViewportWidgetContent(Popup.ToSharedRef(), 1000); } void ULPModInfoSubsystem::CloseWarning() { if (Popup.IsValid() && GEngine && GEngine->GameViewport) { GEngine->GameViewport->RemoveViewportWidgetContent(Popup.ToSharedRef()); } Popup.Reset(); }