// Portage UE 5.5 - voir LPModInfoSubsystem.h #include "LPModInfoSubsystem.h" #include "Blueprint/UserWidget.h" #include "Components/CheckBox.h" #include "Components/HorizontalBox.h" #include "Components/HorizontalBoxSlot.h" #include "Components/ScrollBox.h" #include "Components/ScrollBoxSlot.h" #include "Components/TextBlock.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 "LPDataCompat.h" #include "LPModGuard.h" #include "Misc/EngineVersion.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Styling/CoreStyle.h" #include "UObject/StrProperty.h" #include "UObject/UnrealType.h" #include "UObject/UObjectIterator.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 ModInfoCheckInterval = 2.0; } namespace LPModInfoUi { // Une ligne par controle, dans l'ordre de ELPModCheck. Noms LPModCheck* : onglet Misc (LPSettingsSubsystem::CategoryOf). const TCHAR* const RowNames[] = { TEXT("LPModCheckEngine"), TEXT("LPModCheckMinVersion"), TEXT("LPModCheckMaxVersion") }; const TCHAR* const RowLabels[] = { TEXT("Mods: engine check (restart)"), TEXT("Mods: min version (restart)"), TEXT("Mods: max version (restart)") }; FString RowTip(int32 Index) { switch (Index) { case 0: return TEXT("Refuse mods whose files were built with another Unreal Engine version: they would crash the game. Takes effect at next launch."); case 1: return FString::Printf(TEXT("Refuse mods built for a LifePlay version older than %s (first version on Unreal Engine 5.8). Takes effect at next launch."), FLPModGuard::MinGameVersion); default: return TEXT("Refuse mods built for a newer LifePlay than this game. Takes effect at next launch."); } } // Police des cases du Blueprint (SettingCheckbox_C : TextBlock "Text", taille 20, contour 2), si aucune n'est trouvee. FSlateFontInfo RowFont() { FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Regular", 20); Font.OutlineSettings.OutlineSize = 2; Font.OutlineSettings.bApplyOutlineToDropShadows = true; Font.OutlineSettings.OutlineColor = FLinearColor(0.f, 0.f, 0.f, 0.8f); return Font; } UTextBlock* SampleCheckboxText(UScrollBox* Scroll) { for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { UUserWidget* Row = Cast(Scroll->GetChildAt(Index)); if (Row && Row->GetClass()->GetName().StartsWith(TEXT("SettingCheckbox"))) { if (UTextBlock* Text = Cast(Row->GetWidgetFromName(TEXT("Text")))) { return Text; } } } return nullptr; } // Gestionnaire de mods (U7-T9). UI/ModManager (ModManager_C, liste ModScrollBox) est cree a chaque ouverture par // GI.CreateModManagerWidget (AddToViewport) ; une ligne ModuleTick_C par .lpmod : variable UniqueID (MODULE_UNIQUEID), // NameText (MODULE_NAME), AuthorText (MODULE_AUTHOR), case ModEnabledBox. Le tri (fleches, GI.SortLoadOrder) vide la // liste et y remet les memes lignes : le marquage, porte par les objets, y reste. const TCHAR* const ManagerListName = TEXT("ModScrollBox"); const TCHAR* const ManagerClassPrefix = TEXT("ModManager"); const TCHAR* const RowClassPrefix = TEXT("ModuleTick"); const TCHAR* const NotLoadedSuffix = TEXT(" (not loaded)"); // Rouge attenue : multiplie le blanc du texte, de la case et des fleches de la ligne. Sert aussi de marque (idempotence). constexpr FLinearColor RefusedTint(0.75f, 0.28f, 0.28f, 0.9f); // Ligne d'un mod refuse : teinte, infobulle, suffixe du nom, case desactivee (GI.ModEnableds inchange : le choix du // joueur est conserve). Faux si la ligne porte deja la marque. bool MarkRefusedRow(UUserWidget* Row, const FString& Tip) { if (Row->GetColorAndOpacity() == RefusedTint) { return false; } Row->SetColorAndOpacity(RefusedTint); Row->SetToolTipText(FText::FromString(Tip)); if (UTextBlock* Name = Cast(Row->GetWidgetFromName(TEXT("NameText")))) { const FString Current = Name->GetText().ToString(); if (!Current.EndsWith(NotLoadedSuffix)) { Name->SetText(FText::FromString(Current + NotLoadedSuffix)); } } if (UWidget* Toggle = Row->GetWidgetFromName(TEXT("ModEnabledBox"))) { Toggle->SetIsEnabled(false); } return true; } } void ULPModInfoSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); GameVersion = GetDefault()->ProjectVersion; // Deja fait au demarrage du module LifePlay (FLifePlayGameModule) ; sans effet dans ce cas. FLPModGuard::Run(); // 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); FLPModGuard::ParseLpmod(Text, Keys); 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); // Lot separe du correctif FindCenterOfPolygon : lancer la lecture des ~164 Mo de la ville // des l'instance de jeu, pas dans l'image du clic Continuer. ULPDataCompat::PreloadCityPackages(); // Toujours (lot U7) : cases de Settings > Misc, fenetre si un mod est refuse. TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPModInfoSubsystem::Tick), 0.5f); // Gestionnaire de mods (U7-T9) : la ligne porte MODULE_UNIQUEID (GI, 1re ligne du .lpmod), en regle generale le nom du // plugin (dossier Content/Modules/) ; les deux sont retenus. Aucun mod refuse : pas de ticker. for (const FLPModVerdict& Verdict : FLPModGuard::GetVerdicts()) { if (!Verdict.bRefused) { continue; } const FString Tip = TEXT("Not loaded: ") + (Verdict.Reasons.Num() > 0 ? FString::Join(Verdict.Reasons, TEXT("; ")) : FString(TEXT("see the mod compatibility warning"))); RefusedTips.Add(Verdict.Module, Tip); const FString UniqueId = GetModuleMetadata(Verdict.Module, TEXT("MODULE_UNIQUEID")); if (!UniqueId.IsEmpty()) { RefusedTips.Add(UniqueId, Tip); } } if (RefusedTips.Num() > 0) { // Chaque image, comme LPSettingsSubsystem : la ligne est marquee des la premiere image du gestionnaire. ManagerTickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPModInfoSubsystem::TickModManager), 0.f); } } void ULPModInfoSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); FTSTicker::GetCoreTicker().RemoveTicker(ManagerTickHandle); 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(); } void ULPModInfoSubsystem::OnCheckEngineChanged(bool bIsChecked) { FLPModGuard::SaveCheck(ELPModCheck::Engine, bIsChecked); } void ULPModInfoSubsystem::OnCheckMinVersionChanged(bool bIsChecked) { FLPModGuard::SaveCheck(ELPModCheck::MinVersion, bIsChecked); } void ULPModInfoSubsystem::OnCheckMaxVersionChanged(bool bIsChecked) { FLPModGuard::SaveCheck(ELPModCheck::MaxVersion, bIsChecked); } bool ULPModInfoSubsystem::Tick(float DeltaTime) { (void)DeltaTime; EnsureSettingsRows(); const double Now = FPlatformTime::Seconds(); if (Now < NextCheck || !GEngine || !GEngine->GameViewport || Popup.IsValid()) { return true; } NextCheck = Now + ModInfoCheckInterval; TArray Lines; for (const FLPModVerdict& Verdict : FLPModGuard::GetVerdicts()) { // Coche ou non dans le gestionnaire de mods (lot U7) : le moteur monte un mod meme decoche. if (!Verdict.bRefused || Warned.Contains(Verdict.Module)) { continue; } Warned.Add(Verdict.Module); Lines.Add(FString::Printf(TEXT("- %s (%s): %s"), *Verdict.DisplayName, *Verdict.Module, *FString::Join(Verdict.Reasons, TEXT("; ")))); } if (Lines.Num() > 0) { UE_LOG(LogLifePlay, Display, TEXT("Modules : fenetre d'avertissement, %d mod(s) refuse(s)"), Lines.Num()); ShowWarning(Lines); } return true; } void ULPModInfoSubsystem::EnsureSettingsRows() { UScrollBox* Scroll = nullptr; for (TObjectIterator It; It; ++It) { UScrollBox* Candidate = *It; if (!Candidate || Candidate->GetFName() != TEXT("SettingsScrollBox") || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { continue; } const UUserWidget* Owner = Candidate->GetTypedOuter(); if (!Owner || Owner->HasAnyFlags(RF_ClassDefaultObject) || Owner->GetGameInstance() != GetGameInstance()) { continue; } Scroll = Candidate; break; } if (!Scroll) { return; } for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { const UWidget* Child = Scroll->GetChildAt(Index); if (Child && Child->GetFName() == LPModInfoUi::RowNames[0]) { return; } } const UTextBlock* Sample = LPModInfoUi::SampleCheckboxText(Scroll); const FSlateFontInfo Font = Sample ? Sample->GetFont() : LPModInfoUi::RowFont(); const FSlateColor Color = Sample ? Sample->GetColorAndOpacity() : FSlateColor(FLinearColor(1.f, 1.f, 1.f, 0.95f)); constexpr int32 NumRows = static_cast(ELPModCheck::Num); const FName Handlers[NumRows] = { GET_FUNCTION_NAME_CHECKED(ULPModInfoSubsystem, OnCheckEngineChanged), GET_FUNCTION_NAME_CHECKED(ULPModInfoSubsystem, OnCheckMinVersionChanged), GET_FUNCTION_NAME_CHECKED(ULPModInfoSubsystem, OnCheckMaxVersionChanged) }; for (int32 Index = 0; Index < NumRows; ++Index) { // Meme disposition que SettingCheckbox_C : texte (marges 10 / 20) puis case agrandie x2. UHorizontalBox* Box = NewObject(Scroll, LPModInfoUi::RowNames[Index]); UTextBlock* Caption = NewObject(Box); Caption->SetText(FText::FromString(LPModInfoUi::RowLabels[Index])); Caption->SetFont(Font); Caption->SetColorAndOpacity(Color); UCheckBox* Toggle = NewObject(Box); Toggle->SetIsChecked(FLPModGuard::GetSavedCheck(static_cast(Index))); FWidgetTransform Xform = Toggle->GetRenderTransform(); Xform.Scale = FVector2D(2.f, 2.f); Toggle->SetRenderTransform(Xform); if (UHorizontalBoxSlot* Slot = Box->AddChildToHorizontalBox(Caption)) { Slot->SetPadding(FMargin(10.f, 0.f, 20.f, 0.f)); Slot->SetHorizontalAlignment(HAlign_Left); Slot->SetVerticalAlignment(VAlign_Center); } if (UHorizontalBoxSlot* Slot = Box->AddChildToHorizontalBox(Toggle)) { Slot->SetHorizontalAlignment(HAlign_Left); Slot->SetVerticalAlignment(VAlign_Center); } Box->SetToolTipText(FText::FromString(LPModInfoUi::RowTip(Index))); FScriptDelegate Delegate; Delegate.BindUFunction(this, Handlers[Index]); Toggle->OnCheckStateChanged.Add(Delegate); if (UScrollBoxSlot* Slot = Cast(Scroll->AddChild(Box))) { Slot->SetPadding(FMargin(0.f, 0.f, 0.f, 10.f)); } } UE_LOG(LogLifePlay, Display, TEXT("Modules : cases de controle des mods ajoutees a Settings > Misc (moteur %d, version min %d, version max %d)"), FLPModGuard::GetSavedCheck(ELPModCheck::Engine) ? 1 : 0, FLPModGuard::GetSavedCheck(ELPModCheck::MinVersion) ? 1 : 0, FLPModGuard::GetSavedCheck(ELPModCheck::MaxVersion) ? 1 : 0); } bool ULPModInfoSubsystem::TickModManager(float DeltaTime) { (void)DeltaTime; // Gestionnaire a l'ecran : ModManager_C ajoute au viewport, de cette instance de jeu (une instance retiree attend le GC). UScrollBox* List = nullptr; for (TObjectIterator It; It; ++It) { UScrollBox* Candidate = *It; if (!Candidate || Candidate->GetFName() != LPModInfoUi::ManagerListName || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { continue; } const UUserWidget* Owner = Candidate->GetTypedOuter(); if (!Owner || Owner->HasAnyFlags(RF_ClassDefaultObject) || !Owner->GetClass()->GetName().StartsWith(LPModInfoUi::ManagerClassPrefix) || !Owner->IsInViewport() || Owner->GetGameInstance() != GetGameInstance()) { continue; } List = Candidate; break; } if (!List) { return true; } if (List != ManagerList.Get()) { ManagerList = List; bManagerListLogged = false; } // Lignes deja marquees : sautees (MarkRefusedRow), donc rien a refaire apres un tri ou a l'image suivante. int32 Marked = 0; const int32 NumRows = List->GetChildrenCount(); for (int32 Index = 0; Index < NumRows; ++Index) { UUserWidget* Row = Cast(List->GetChildAt(Index)); if (!Row || !Row->GetClass()->GetName().StartsWith(LPModInfoUi::RowClassPrefix)) { continue; } const FStrProperty* IdProperty = CastField(Row->GetClass()->FindPropertyByName(TEXT("UniqueID"))); if (!IdProperty) { if (!bWarnedNoUniqueId) { bWarnedNoUniqueId = true; UE_LOG(LogLifePlay, Warning, TEXT("Modules : gestionnaire, variable UniqueID absente de %s : lignes des mods refuses non marquees"), *Row->GetClass()->GetName()); } continue; } const FString* Tip = RefusedTips.Find(IdProperty->GetPropertyValue_InContainer(Row)); if (Tip && LPModInfoUi::MarkRefusedRow(Row, *Tip)) { ++Marked; } } // Une ligne par ouverture (0 compris : aucun mod refuse dans la liste), puis seulement si de nouvelles lignes sont marquees. if (Marked > 0 || (!bManagerListLogged && NumRows > 0)) { UE_LOG(LogLifePlay, Display, TEXT("Modules : gestionnaire, %d mod(s) refuse(s) marque(s)"), Marked); bManagerListLogged = true; } return true; } void ULPModInfoSubsystem::ShowWarning(const TArray& Lines) { const FEngineVersion& CurrentEngine = FEngineVersion::Current(); const FString Body = FString::Printf(TEXT("This game is LifePlay %s (Unreal Engine %d.%d). The following mod(s) were not loaded: none of ") TEXT("their assets can be used, so that the game does not crash.\n\n%s\n\nUpdate the mod, or ask its author for a version ") TEXT("built for LifePlay %s. To load a mod anyway (the game may crash), uncheck the matching check in Settings > Misc ") TEXT("(Mods: engine check, Mods: min version, Mods: max version), then restart the game."), *GameVersion, static_cast(CurrentEngine.GetMajor()), static_cast(CurrentEngine.GetMinor()), *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 compatibility 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(); }