LifePlay-Revival/Source/LifePlay/LPModInfoSubsystem.cpp

203 lines
6.6 KiB
C++
Raw Normal View History

// 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<FProperty> 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<UGeneralProjectSettings>()->ProjectVersion;
// 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);
TArray<FString> 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<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();
}
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<FArrayProperty>(GI->GetClass()->FindPropertyByName(TEXT("ModEnableds"))) : nullptr;
const FStructProperty* Inner = Prop ? CastField<FStructProperty>(Prop->Inner) : nullptr;
if (!Inner)
{
return false;
}
FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(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<FString> Lines;
for (const TPair<FString, TMap<FString, FString>>& 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<FString>& 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();
}