LifePlay-Revival/Source/LifePlay/LPModInfoSubsystem.cpp
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

442 lines
16 KiB
C++

// 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<UUserWidget>(Scroll->GetChildAt(Index));
if (Row && Row->GetClass()->GetName().StartsWith(TEXT("SettingCheckbox")))
{
if (UTextBlock* Text = Cast<UTextBlock>(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<UTextBlock>(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<UGeneralProjectSettings>()->ProjectVersion;
// Deja fait au demarrage du module LifePlay (FLifePlayGameModule) ; sans effet dans ce cas.
FLPModGuard::Run();
// Modules/<id>/<id>.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<FString> 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<FString, FString>& 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/<id>) ; 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<FString, TMap<FString, FString>>& 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<FString> 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<UScrollBox> It; It; ++It)
{
UScrollBox* Candidate = *It;
if (!Candidate || Candidate->GetFName() != TEXT("SettingsScrollBox") || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
{
continue;
}
const UUserWidget* Owner = Candidate->GetTypedOuter<UUserWidget>();
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<int32>(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<UHorizontalBox>(Scroll, LPModInfoUi::RowNames[Index]);
UTextBlock* Caption = NewObject<UTextBlock>(Box);
Caption->SetText(FText::FromString(LPModInfoUi::RowLabels[Index]));
Caption->SetFont(Font);
Caption->SetColorAndOpacity(Color);
UCheckBox* Toggle = NewObject<UCheckBox>(Box);
Toggle->SetIsChecked(FLPModGuard::GetSavedCheck(static_cast<ELPModCheck>(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<UScrollBoxSlot>(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<UScrollBox> It; It; ++It)
{
UScrollBox* Candidate = *It;
if (!Candidate || Candidate->GetFName() != LPModInfoUi::ManagerListName || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
{
continue;
}
const UUserWidget* Owner = Candidate->GetTypedOuter<UUserWidget>();
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<UUserWidget>(List->GetChildAt(Index));
if (!Row || !Row->GetClass()->GetName().StartsWith(LPModInfoUi::RowClassPrefix))
{
continue;
}
const FStrProperty* IdProperty = CastField<FStrProperty>(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<FString>& 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<int32>(CurrentEngine.GetMajor()), static_cast<int32>(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();
}