LifePlay-Revival/Source/LifePlay/LPModGuard.cpp

544 lines
20 KiB
C++
Raw Permalink Normal View History

// Lot U7 (U7-T7) - voir LPModGuard.h
//
// API moteur lues dans UE 5.8.3 (Engine/Source) : montage des plugins PluginManager.cpp:1965-1979 et :2015-2018 ;
// UnmountExplicitlyLoadedPlugin (reserve aux plugins ExplicitlyLoaded) :3650-3739 ; FPackageName::UnRegisterMountPoint
// PackageName.h:347, PackageName.cpp:426-462 ; purge du registre AssetRegistry.cpp:9241-9327 ; FPakPlatformFile::Unmount
// IPlatformFilePak.cpp:6288-6356, delegue OnUnmountPak CoreDelegates.h:128, IPlatformFilePak.cpp:5808.
#include "LPModGuard.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "CoreGlobals.h"
#include "HAL/Event.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/PlatformProperties.h"
#include "Interfaces/IPluginManager.h"
#include "IO/IoChunkId.h"
#include "IO/IoContainerHeader.h"
#include "IO/IoContainerId.h"
#include "IO/IoDispatcher.h"
#include "LifePlay.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/CoreDelegates.h"
#include "Misc/EngineVersion.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
// A relever au prochain changement de moteur (premiere version du jeu cuite par ce moteur).
const TCHAR* const FLPModGuard::MinGameVersion = TEXT("2026.10.2.0");
namespace LPModGuardImpl
{
constexpr int32 NumChecks = static_cast<int32>(ELPModCheck::Num);
const TCHAR* const SettingsSection = TEXT("/Script/LifePlay.LPModInfoSubsystem");
const TCHAR* const CheckKeys[NumChecks] = { TEXT("bCheckModEngine"), TEXT("bCheckModMinVersion"), TEXT("bCheckModMaxVersion") };
const TCHAR* const CheckNames[NumChecks] = { TEXT("moteur"), TEXT("version min"), TEXT("version max") };
bool GRan = false;
bool GActive[NumChecks] = { true, true, true };
FString GGameVersion;
TArray<FLPModVerdict> GVerdicts;
struct FFailure
{
ELPModCheck Check = ELPModCheck::Engine;
FString En;
FString Fr;
};
/** "2026.10.2.0" -> {2026, 10, 2, 0} ; faux si une composante n'est pas un entier (comparaison numerique, pas lexicale).
* Lot REL-ALPHA (decision de l'utilisateur "Lettre acceptee", 2026-10-14) : la derniere composante peut finir par des
* lettres ASCII a-z / A-Z, retirees avant les controles et ignorees a la comparaison : "2026.10.2.1a" -> {2026, 10, 2, 1}. */
bool ParseVersion(const FString& Text, TArray<int64>& Out)
{
Out.Reset();
TArray<FString> Parts;
Text.TrimStartAndEnd().ParseIntoArray(Parts, TEXT("."), false);
// Lettres de fin de la derniere composante seulement, par plages explicites (aucune lettre non ASCII retiree).
if (Parts.Num() > 0)
{
FString& LastPart = Parts.Last();
int32 Keep = LastPart.Len();
while (Keep > 0)
{
const TCHAR C = LastPart[Keep - 1];
if (!((C >= TEXT('a') && C <= TEXT('z')) || (C >= TEXT('A') && C <= TEXT('Z'))))
{
break;
}
--Keep;
}
LastPart.LeftInline(Keep);
}
for (const FString& Part : Parts)
{
if (Part.IsEmpty() || Part.Len() > 9)
{
return false;
}
for (const TCHAR C : Part)
{
if (!FChar::IsDigit(C))
{
return false;
}
}
Out.Add(FCString::Atoi64(*Part));
}
return Out.Num() > 0;
}
/** <0, 0, >0 ; une composante absente vaut 0 (2026.10.4 == 2026.10.4.0). */
int32 CompareVersions(const TArray<int64>& A, const TArray<int64>& B)
{
const int32 Count = FMath::Max(A.Num(), B.Num());
for (int32 Index = 0; Index < Count; ++Index)
{
const int64 Left = A.IsValidIndex(Index) ? A[Index] : 0;
const int64 Right = B.IsValidIndex(Index) ? B[Index] : 0;
if (Left != Right)
{
return Left < Right ? -1 : 1;
}
}
return 0;
}
bool ReadCheck(int32 Index)
{
bool bValue = true;
if (GConfig)
{
GConfig->GetBool(SettingsSection, CheckKeys[Index], bValue, GGameUserSettingsIni);
}
return bValue;
}
/** Paks du plugin : meme recherche que FPluginManager au montage (PluginManager.cpp:1965-1979), donc les memes
* chaines que celles passees a FCoreDelegates::MountPak (FPakPlatformFile::Unmount compare le nom exact). */
TArray<FString> FindPluginPaks(const IPlugin& Plugin)
{
TArray<FString> Paks;
const FString Dir = Plugin.GetContentDir() / TEXT("Paks") / FString(FPlatformProperties::PlatformName());
FPlatformFileManager::Get().GetPlatformFile().IterateDirectoryRecursively(*Dir, [&Paks](const TCHAR* Name, bool bIsDirectory)
{
const FString File(Name);
if (!bIsDirectory && File.MatchesWildcard(TEXT("*.pak")))
{
Paks.AddUnique(File);
}
return true;
});
return Paks;
}
/** Bloc ContainerHeader du conteneur IoStore d'un pak : identifiant = FromName(nom de base du .utoc) (U7-T6 section 3,
* verifie sur 6 conteneurs ; Id du journal T4). */
FIoChunkId HeaderChunkOf(const FString& PakPath)
{
return CreateContainerHeaderChunkId(FIoContainerId::FromName(FName(*FPaths::GetBaseFilename(PakPath))));
}
/** Version de l'en-tete de conteneur (Signature puis Version, IoContainerHeader.cpp:51-58) ; -1 si illisible. */
int32 ReadContainerHeaderVersion(const FString& PakPath)
{
if (!FIoDispatcher::IsInitialized() || !IFileManager::Get().FileExists(*FPaths::ChangeExtension(PakPath, TEXT("utoc"))))
{
return -1;
}
const FIoChunkId Chunk = HeaderChunkOf(PakPath);
FIoDispatcher& Dispatcher = FIoDispatcher::Get();
if (!Dispatcher.DoesChunkExist(Chunk))
{
return -1;
}
FIoBatch Batch = Dispatcher.NewBatch();
FIoRequest Request = Batch.Read(Chunk, FIoReadOptions(0, 8), IoDispatcherPriority_High);
FEventRef Done;
Batch.IssueAndTriggerEvent(Done.Get());
Done->Wait();
// Nul en cas d'echec (IoDispatcher.cpp:1443-1461) : pas de GetResultOrDie, qui arreterait le jeu.
const FIoBuffer* Buffer = Request.GetResult();
if (!Buffer || Buffer->DataSize() < 8)
{
return -1;
}
uint32 Words[2] = { 0, 0 };
FMemory::Memcpy(Words, Buffer->Data(), sizeof(Words));
return Words[0] == static_cast<uint32>(FIoContainerHeader::Signature) ? static_cast<int32>(Words[1]) : -1;
}
/** Version de Mods/<id>/AssetRegistry.bin (dans le pak du mod, meme fichier que le registre d'assets du moteur,
* AssetRegistry.cpp:642) ; -1 si illisible. */
int32 ReadAssetRegistryVersion(const IPlugin& Plugin)
{
TUniquePtr<FArchive> Reader(IFileManager::Get().CreateFileReader(*(Plugin.GetBaseDir() / TEXT("AssetRegistry.bin")), FILEREAD_Silent));
if (!Reader)
{
return -1;
}
FAssetRegistryVersion::Type Version = FAssetRegistryVersion::PreVersioning;
if (!FAssetRegistryVersion::SerializeVersion(*Reader, Version) || Reader->IsError())
{
return -1;
}
return static_cast<int32>(Version);
}
/** Retire le point de montage /<id>/ puis demonte les paks du plugin ; vrai si rien du mod n'est plus accessible. */
bool UnloadPlugin(const IPlugin& Plugin, const TArray<FString>& Paks)
{
const FString Root = Plugin.GetMountedAssetPath();
// Etape de contenu de FPluginManager::UnmountExplicitlyLoadedPlugin (PluginManager.cpp:3716), fonction refusee ici
// car le plugin n'est pas ExplicitlyLoaded (:3660-3667) ; memes arguments qu'au montage (:2015-2018). Le registre
// d'assets retire alors tous les assets du point de montage (AssetRegistry.cpp:9241-9327 ; chemin non persistant :
// UpdatePersistentMountPoints ne voit que le registre principal, :623, avant ceux des plugins, :633-656).
FPackageName::UnRegisterMountPoint(Root, Plugin.GetContentDir());
// Pak, entrees du magasin de paquets et conteneur IoStore (FPakPlatformFile::Unmount, IPlatformFilePak.cpp:6288-6356).
int32 Unmounted = 0;
for (const FString& Pak : Paks)
{
if (FCoreDelegates::OnUnmountPak.IsBound() && FCoreDelegates::OnUnmountPak.Execute(Pak))
{
++Unmounted;
}
}
// Controle relu apres coup.
const bool bRootGone = !FPackageName::MountPointExists(Root);
const IAssetRegistry* Registry = IAssetRegistry::Get();
const bool bRegistryEmpty = !Registry || !Registry->HasAssets(FName(*Root.LeftChop(1)), true);
bool bContainersGone = true;
for (const FString& Pak : Paks)
{
if (FIoDispatcher::IsInitialized() && FIoDispatcher::Get().DoesChunkExist(HeaderChunkOf(Pak)))
{
bContainersGone = false;
}
}
const FString Report = FString::Printf(TEXT("%d pak(s) demonte(s) sur %d, point de montage %s %s, registre %s, conteneur(s) %s"),
Unmounted, Paks.Num(), *Root, bRootGone ? TEXT("retire") : TEXT("ENCORE PRESENT"),
bRegistryEmpty ? TEXT("vide") : TEXT("NON VIDE"), bContainersGone ? TEXT("absent(s)") : TEXT("ENCORE PRESENT(S)"));
const bool bOk = Unmounted == Paks.Num() && bRootGone && bRegistryEmpty && bContainersGone;
if (bOk)
{
UE_LOG(LogLifePlay, Display, TEXT("Modules : %s non charge : %s"), *Plugin.GetName(), *Report);
}
else
{
UE_LOG(LogLifePlay, Error, TEXT("Modules : %s demontage incomplet : %s ; ses assets ne sont ecartes que par LPModDailies et LPHairstyleSubsystem"),
*Plugin.GetName(), *Report);
}
return bOk;
}
bool HasFailure(const TArray<FFailure>& Failures, ELPModCheck Check)
{
return Failures.ContainsByPredicate([Check](const FFailure& Failure) { return Failure.Check == Check; });
}
}
void FLPModGuard::Run()
{
using namespace LPModGuardImpl;
if (GRan)
{
return;
}
GRan = true;
// Cuisson, resave, outils : aucun mod de joueur a controler.
if (IsRunningCommandlet() || !GConfig)
{
return;
}
// Meme valeur que UGeneralProjectSettings::ProjectVersion, lue sans dependre de l'objet par defaut (demarrage du module).
GConfig->GetString(TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectVersion"), GGameVersion, GGameIni);
for (int32 Index = 0; Index < NumChecks; ++Index)
{
GActive[Index] = ReadCheck(Index);
}
TArray<TSharedRef<IPlugin>> Mods;
for (const TSharedRef<IPlugin>& Plugin : IPluginManager::Get().GetEnabledPluginsWithContent())
{
if (Plugin->GetType() == EPluginType::Mod)
{
Mods.Add(Plugin);
}
}
const bool bCooked = FPlatformProperties::RequiresCookedData();
const int32 GameContainer = static_cast<int32>(EIoContainerHeaderVersion::Latest);
const int32 GameRegistry = static_cast<int32>(FAssetRegistryVersion::LatestVersion);
const FEngineVersion& CurrentEngine = FEngineVersion::Current();
const int32 EngineMajor = static_cast<int32>(CurrentEngine.GetMajor());
const int32 EngineMinor = static_cast<int32>(CurrentEngine.GetMinor());
UE_LOG(LogLifePlay, Display, TEXT("Modules : garde des mods d'assets, %d plugin(s) de type Mod, jeu %s (version min des mods %s), moteur %s (conteneur %d, registre %d)%s"),
Mods.Num(), *GGameVersion, MinGameVersion, *CurrentEngine.ToString(), GameContainer, GameRegistry,
bCooked ? TEXT("") : TEXT(", jeu non cuit : controle binaire sans objet, rien n'est demonte"));
for (int32 Index = 0; Index < NumChecks; ++Index)
{
if (!GActive[Index])
{
UE_LOG(LogLifePlay, Warning, TEXT("Modules : controle %s des mods leve (Settings > Misc, %s)"), CheckNames[Index], *GGameUserSettingsIni);
}
}
TArray<int64> GameParts;
TArray<int64> MinParts;
const bool bGameParsed = ParseVersion(GGameVersion, GameParts);
ParseVersion(MinGameVersion, MinParts);
if (!bGameParsed && Mods.Num() > 0)
{
UE_LOG(LogLifePlay, Error, TEXT("Modules : ProjectVersion '%s' illisible, controle version max sans objet"), *GGameVersion);
}
TArray<TArray<FString>> ModPaks;
for (const TSharedRef<IPlugin>& Plugin : Mods)
{
FLPModVerdict Verdict;
Verdict.Module = Plugin->GetName();
TMap<FString, FString> Keys;
FString Text;
if (FFileHelper::LoadFileToString(Text, *(FPaths::ProjectContentDir() / TEXT("Modules") / Verdict.Module / Verdict.Module + TEXT(".lpmod"))))
{
ParseLpmod(Text, Keys);
}
const FString* Name = Keys.Find(TEXT("MODULE_NAME"));
Verdict.DisplayName = Name && !Name->IsEmpty() ? *Name : Plugin->GetFriendlyName();
if (Verdict.DisplayName.IsEmpty())
{
Verdict.DisplayName = Verdict.Module;
}
Verdict.BuiltFor = Keys.FindRef(TEXT("MODULE_GAME_VERSION"));
TArray<FFailure> Failures;
// 1. Moteur : format binaire (jeu cuit), puis EngineVersion du .uplugin.
TArray<FString> Paks = bCooked ? FindPluginPaks(*Plugin) : TArray<FString>();
FString Formats = bCooked ? TEXT("aucun pak") : TEXT("format non lu");
if (Paks.Num() > 0)
{
const int32 RegistryVersion = ReadAssetRegistryVersion(*Plugin);
int32 ContainerVersion = GameContainer;
FString Unreadable = RegistryVersion < 0 ? TEXT("AssetRegistry.bin") : TEXT("");
for (const FString& Pak : Paks)
{
const int32 PakContainerVersion = ReadContainerHeaderVersion(Pak);
if (PakContainerVersion < 0)
{
Unreadable += (Unreadable.IsEmpty() ? TEXT("") : TEXT(", ")) + FPaths::GetBaseFilename(Pak) + TEXT(".utoc");
}
else if (PakContainerVersion != GameContainer)
{
ContainerVersion = PakContainerVersion;
}
}
Formats = FString::Printf(TEXT("conteneur %d, registre %d"), ContainerVersion, RegistryVersion);
if (!Unreadable.IsEmpty())
{
Failures.Add(FFailure{ ELPModCheck::Engine, TEXT("mod files unreadable"),
FString::Printf(TEXT("moteur : fichiers illisibles (%s)"), *Unreadable) });
}
else if (ContainerVersion != GameContainer || RegistryVersion != GameRegistry)
{
const bool bByContainer = ContainerVersion != GameContainer;
const int32 ModFormat = bByContainer ? ContainerVersion : RegistryVersion;
const int32 GameFormat = bByContainer ? GameContainer : GameRegistry;
Failures.Add(FFailure{ ELPModCheck::Engine,
FString::Printf(TEXT("built with %s engine (%sformat %d, game %d): rebuild the mod for LifePlay %s"),
ModFormat < GameFormat ? TEXT("an older") : TEXT("a newer"), bByContainer ? TEXT("") : TEXT("asset registry "),
ModFormat, GameFormat, *GGameVersion),
FString::Printf(TEXT("moteur : conteneur %d (jeu %d), registre %d (jeu %d)"), ContainerVersion, GameContainer,
RegistryVersion, GameRegistry) });
}
}
const FString& Declared = Plugin->GetDescriptor().EngineVersion;
if (!Declared.IsEmpty() && !HasFailure(Failures, ELPModCheck::Engine))
{
FEngineVersion Parsed;
if (!FEngineVersion::Parse(Declared, Parsed))
{
Failures.Add(FFailure{ ELPModCheck::Engine, TEXT("mod files unreadable"),
FString::Printf(TEXT("moteur : EngineVersion '%s' illisible dans le .uplugin"), *Declared) });
}
else if (static_cast<int32>(Parsed.GetMajor()) != EngineMajor || static_cast<int32>(Parsed.GetMinor()) != EngineMinor)
{
const bool bOlder = static_cast<int32>(Parsed.GetMajor()) < EngineMajor
|| (static_cast<int32>(Parsed.GetMajor()) == EngineMajor && static_cast<int32>(Parsed.GetMinor()) < EngineMinor);
Failures.Add(FFailure{ ELPModCheck::Engine,
FString::Printf(TEXT("built with %s engine (Unreal Engine %s, game %d.%d): rebuild the mod for LifePlay %s"),
bOlder ? TEXT("an older") : TEXT("a newer"), *Declared, EngineMajor, EngineMinor, *GGameVersion),
FString::Printf(TEXT("moteur : EngineVersion %s (moteur %d.%d)"), *Declared, EngineMajor, EngineMinor) });
}
}
// 2 et 3. Versions min et max du jeu visees par le mod.
if (Verdict.BuiltFor.IsEmpty())
{
UE_LOG(LogLifePlay, Warning, TEXT("Modules : %s sans MODULE_GAME_VERSION, controles de version min et max sans objet"), *Verdict.Module);
}
else
{
TArray<int64> BuiltParts;
if (!ParseVersion(Verdict.BuiltFor, BuiltParts))
{
Failures.Add(FFailure{ ELPModCheck::MinVersion,
FString::Printf(TEXT("unreadable game version '%s' in its .lpmod"), *Verdict.BuiltFor),
FString::Printf(TEXT("version min : MODULE_GAME_VERSION '%s' illisible"), *Verdict.BuiltFor) });
}
else
{
if (CompareVersions(BuiltParts, MinParts) < 0)
{
Failures.Add(FFailure{ ELPModCheck::MinVersion,
FString::Printf(TEXT("built for LifePlay %s, minimum is %s"), *Verdict.BuiltFor, MinGameVersion),
FString::Printf(TEXT("version min : construit pour LifePlay %s < %s"), *Verdict.BuiltFor, MinGameVersion) });
}
if (bGameParsed && CompareVersions(BuiltParts, GameParts) > 0)
{
Failures.Add(FFailure{ ELPModCheck::MaxVersion,
FString::Printf(TEXT("built for LifePlay %s, this game is %s: update the game"), *Verdict.BuiltFor, *GGameVersion),
FString::Printf(TEXT("version max : construit pour LifePlay %s > %s"), *Verdict.BuiltFor, *GGameVersion) });
}
}
}
// Echec Moteur actif : les echecs min/max restent au journal seulement (secondaires). La numerotation 5.5 n'est pas
// monotone (mods 2026.10.4 > 2026.10.2.0) : la fenetre dirait a tort "update the game".
const bool bEngineRefused = GActive[static_cast<int32>(ELPModCheck::Engine)] && HasFailure(Failures, ELPModCheck::Engine);
TArray<FString> Lifted;
for (const FFailure& Failure : Failures)
{
if (GActive[static_cast<int32>(Failure.Check)])
{
Verdict.bRefused = true;
if (bEngineRefused && Failure.Check != ELPModCheck::Engine)
{
Verdict.LogReasons.Add(Failure.Fr + TEXT(" (secondaire)"));
}
else
{
Verdict.Reasons.Add(Failure.En);
Verdict.LogReasons.Add(Failure.Fr);
}
}
else
{
Lifted.Add(Failure.Fr);
}
}
const FString Summary = FString::Printf(TEXT("%s, EngineVersion %s, construit pour LifePlay %s"), *Formats,
Declared.IsEmpty() ? TEXT("absent") : *Declared, Verdict.BuiltFor.IsEmpty() ? TEXT("?") : *Verdict.BuiltFor);
const FString LiftedNote = Lifted.Num() > 0 ? TEXT(" ; controle leve : ") + FString::Join(Lifted, TEXT(" ; ")) : FString();
if (Verdict.bRefused)
{
// Une ligne Warning par mod refuse.
UE_LOG(LogLifePlay, Warning, TEXT("Modules : %s refuse : %s%s"), *Verdict.Module, *FString::Join(Verdict.LogReasons, TEXT(" ; ")),
*LiftedNote);
}
else if (Lifted.Num() > 0)
{
UE_LOG(LogLifePlay, Warning, TEXT("Modules : %s charge malgre : %s (controle leve dans Settings > Misc)"), *Verdict.Module,
*FString::Join(Lifted, TEXT(" ; ")));
}
else
{
UE_LOG(LogLifePlay, Display, TEXT("Modules : %s accepte (%s)"), *Verdict.Module, *Summary);
}
GVerdicts.Add(MoveTemp(Verdict));
ModPaks.Add(MoveTemp(Paks));
}
// Jeu cuit : un mod refuse n'est pas charge. L'editeur ne demonte rien (contenu non cuit, pas de pak).
if (!bCooked)
{
return;
}
bool bWaited = false;
for (int32 Index = 0; Index < GVerdicts.Num(); ++Index)
{
if (!GVerdicts[Index].bRefused)
{
continue;
}
if (!bWaited)
{
// Le registre des plugins peut se charger en tache de fond (AssetRegistry.cpp:377-428) : il doit etre complet
// avant le retrait du point de montage, sinon celui du mod serait ajoute apres la purge.
if (IAssetRegistry* Registry = IAssetRegistry::Get())
{
Registry->WaitForPremadeAssetRegistry();
}
bWaited = true;
}
GVerdicts[Index].bUnloaded = UnloadPlugin(*Mods[Index], ModPaks[Index]);
}
}
const TArray<FLPModVerdict>& FLPModGuard::GetVerdicts()
{
Run();
return LPModGuardImpl::GVerdicts;
}
bool FLPModGuard::IsRefused(const FString& Module)
{
for (const FLPModVerdict& Verdict : GetVerdicts())
{
if (Verdict.bRefused && Verdict.Module.Equals(Module, ESearchCase::IgnoreCase))
{
return true;
}
}
return false;
}
bool FLPModGuard::IsCheckActive(ELPModCheck Check)
{
Run();
const int32 Index = static_cast<int32>(Check);
return Index >= 0 && Index < LPModGuardImpl::NumChecks ? LPModGuardImpl::GActive[Index] : true;
}
bool FLPModGuard::GetSavedCheck(ELPModCheck Check)
{
const int32 Index = static_cast<int32>(Check);
return Index >= 0 && Index < LPModGuardImpl::NumChecks ? LPModGuardImpl::ReadCheck(Index) : true;
}
void FLPModGuard::SaveCheck(ELPModCheck Check, bool bEnabled)
{
using namespace LPModGuardImpl;
const int32 Index = static_cast<int32>(Check);
if (Index < 0 || Index >= NumChecks || !GConfig)
{
return;
}
GConfig->SetBool(SettingsSection, CheckKeys[Index], bEnabled, GGameUserSettingsIni);
GConfig->Flush(false, GGameUserSettingsIni);
UE_LOG(LogLifePlay, Display, TEXT("Modules : controle %s des mods %s dans %s, effet au prochain lancement"), CheckNames[Index],
bEnabled ? TEXT("active") : TEXT("leve"), *GGameUserSettingsIni);
}
const FString& FLPModGuard::GetGameVersion()
{
Run();
return LPModGuardImpl::GGameVersion;
}
void FLPModGuard::ParseLpmod(const FString& Text, TMap<FString, FString>& OutKeys)
{
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))
{
OutKeys.Add(Key.TrimStartAndEnd().ToUpper(), Value.TrimStartAndEnd());
}
}
}