2395 lines
80 KiB
C++
2395 lines
80 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "OnlineHotfixManager.h"
|
|
#include "Online.h"
|
|
#include "OnlineSubsystemUtils.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/Package.h"
|
|
|
|
#include "HAL/ConsoleManager.h"
|
|
|
|
#include "Misc/PackageName.h"
|
|
#include "Misc/EngineVersion.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Misc/App.h"
|
|
#include "Misc/ConfigUtilities.h"
|
|
|
|
#include "MoviePlayerProxy.h"
|
|
|
|
#include "Engine/CurveTable.h"
|
|
#include "Engine/DataTable.h"
|
|
#include "Curves/CurveFloat.h"
|
|
#include "Curves/CurveVector.h"
|
|
#include "Curves/CurveLinearColor.h"
|
|
#include "Engine/BlueprintGeneratedClass.h"
|
|
|
|
#include "AutoRTFM.h"
|
|
#include "Serialization/AsyncLoadingFlushContext.h"
|
|
#include "UObject/PropertyAccessUtil.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(OnlineHotfixManager)
|
|
|
|
DEFINE_LOG_CATEGORY(LogHotfixManager);
|
|
|
|
/** This character must be between important pieces of file information (platform, initype, version) */
|
|
#define HOTFIX_SEPARATOR TEXT("_")
|
|
/** The prefix for any hotfix file that expects to indicate version information */
|
|
#define HOTFIX_VERSION_TAG TEXT("Ver-")
|
|
/** The prefix for any hotfix file that expects to indicate branch version information */
|
|
#define HOTFIX_BRANCH_VERSION_TAG TEXT("Branch-")
|
|
|
|
FName NAME_HotfixManager(TEXT("HotfixManager"));
|
|
|
|
static TAutoConsoleVariable<int32> CVarUseNewDynamicLayersForHotfix(
|
|
TEXT("ini.UseNewDynamicLayersForHotfix"),
|
|
1,
|
|
TEXT("If true, use the new dynamic layers that load/unload configs, specifically for Hotfixes"),
|
|
ECVF_Default);
|
|
|
|
static TAutoConsoleVariable<bool> CVarEmptyDynamicHotfixContentsOnNewHotfix(
|
|
TEXT("hotfix.EmptyDynamicHotfixContentsOnNewHotfix"),
|
|
false,
|
|
TEXT("If true clear out the existing values for DynamicHotfixContents when a new hotfix is processed. When set to false only remove the files which were removed since the last hotfix."),
|
|
ECVF_Default);
|
|
|
|
|
|
class FPakFileVisitor : public IPlatformFile::FDirectoryVisitor
|
|
{
|
|
public:
|
|
virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override
|
|
{
|
|
if (!bIsDirectory)
|
|
{
|
|
Files.Add(FilenameOrDirectory);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
TArray<FString> Files;
|
|
};
|
|
|
|
namespace OnlineHotfixManagerCVars
|
|
{
|
|
static bool bDeferBroadcastCurveTableModified = true;
|
|
static FAutoConsoleVariableRef DeferBroadcastCurveTableModifiedCVar(
|
|
TEXT("hotfix.DeferBroadcastCurveTableModified"),
|
|
bDeferBroadcastCurveTableModified,
|
|
TEXT("Whether to wait until all asset hotfixes have been applied before broadcasting OnCurveTableChanged delegates, as opposed to broadcasting after each individual modification"),
|
|
ECVF_Default);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
/** @return the expected network version for hotfix files determined at compile time */
|
|
FString GetNetworkVersion()
|
|
{
|
|
static FString NetVerStr;
|
|
if (NetVerStr.IsEmpty())
|
|
{
|
|
uint32 NetVer = FNetworkVersion::GetNetworkCompatibleChangelist();
|
|
NetVerStr = FString::Printf(TEXT("%s%d"), HOTFIX_VERSION_TAG, NetVer);
|
|
}
|
|
return NetVerStr;
|
|
}
|
|
|
|
/** @return the expected branch version for hotfix files determined at compile time */
|
|
FString GetBranchVersion()
|
|
{
|
|
static FString BranchVersion;
|
|
if (BranchVersion.IsEmpty())
|
|
{
|
|
BranchVersion = HOTFIX_BRANCH_VERSION_TAG + FPaths::GetCleanFilename(FEngineVersion::Current().GetBranch());
|
|
}
|
|
return BranchVersion;
|
|
}
|
|
|
|
/**
|
|
* Given a hotfix file name, return the file name with version stripped out and exposed separately
|
|
*
|
|
* @param InFilename name of file to search for version information
|
|
* @param OutFilename name with version information removed
|
|
* @param OutNetVersion version of the hotfix file it present in the name
|
|
* @param OutBranchVersion version of the hotfix file it present in the name
|
|
*/
|
|
void GetFilenameAndVersion(const FString& InFilename, FString& OutFilename, FString& OutNetVersion, FString& OutBranchVersion)
|
|
{
|
|
TArray<FString> FileParts;
|
|
int32 NumTokens = InFilename.ParseIntoArray(FileParts, HOTFIX_SEPARATOR);
|
|
if (NumTokens > 0)
|
|
{
|
|
for (int i = 0; i < FileParts.Num(); i++)
|
|
{
|
|
if (FileParts[i].StartsWith(HOTFIX_VERSION_TAG))
|
|
{
|
|
OutNetVersion = FileParts[i];
|
|
}
|
|
else if (FileParts[i].StartsWith(HOTFIX_BRANCH_VERSION_TAG))
|
|
{
|
|
OutBranchVersion = FileParts[i];
|
|
}
|
|
else
|
|
{
|
|
OutFilename += FileParts[i];
|
|
if (i < FileParts.Num() - 1)
|
|
{
|
|
OutFilename += HOTFIX_SEPARATOR;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UOnlineHotfixManager::IsCompatibleHotfixFile(const FString& InFilename, FString& OutFilename)
|
|
{
|
|
bool bHasNetVersion = false;
|
|
bool bCompatibleNetHotfix = false;
|
|
bool bHasBranchVersion = false;
|
|
bool bCompatibleBranchHotfix = false;
|
|
FString OutNetVersion;
|
|
FString OutBranchVersion;
|
|
GetFilenameAndVersion(InFilename, OutFilename, OutNetVersion, OutBranchVersion);
|
|
|
|
if (!OutNetVersion.IsEmpty())
|
|
{
|
|
bHasNetVersion = true;
|
|
const FString NetworkVersion = GetNetworkVersion();
|
|
if (OutNetVersion == NetworkVersion)
|
|
{
|
|
bCompatibleNetHotfix = true;
|
|
}
|
|
}
|
|
|
|
if (!OutBranchVersion.IsEmpty())
|
|
{
|
|
bHasBranchVersion = true;
|
|
const FString BranchVersion = GetBranchVersion();
|
|
if (OutBranchVersion == BranchVersion)
|
|
{
|
|
bCompatibleBranchHotfix = true;
|
|
}
|
|
}
|
|
|
|
return (bCompatibleNetHotfix || !bHasNetVersion) && (bCompatibleBranchHotfix || !bHasBranchVersion);
|
|
}
|
|
|
|
UOnlineHotfixManager::UOnlineHotfixManager() :
|
|
Super(),
|
|
TotalFiles(0),
|
|
NumDownloaded(0),
|
|
TotalBytes(0),
|
|
NumBytes(0),
|
|
bHotfixingInProgress(false),
|
|
bHotfixNeedsMapReload(false),
|
|
ChangedOrRemovedPakCount(0)
|
|
{
|
|
OnEnumerateFilesCompleteDelegate = FOnEnumerateFilesCompleteDelegate::CreateUObject(this, &UOnlineHotfixManager::OnEnumerateFilesComplete);
|
|
OnReadFileProgressDelegate = FOnReadFileProgressDelegate::CreateUObject(this, &UOnlineHotfixManager::OnReadFileProgress);
|
|
OnReadFileCompleteDelegate = FOnReadFileCompleteDelegate::CreateUObject(this, &UOnlineHotfixManager::OnReadFileComplete);
|
|
#if !UE_BUILD_SHIPPING
|
|
bLogMountedPakContents = FParse::Param(FCommandLine::Get(), TEXT("LogHotfixPakContents"));
|
|
#endif
|
|
GameContentPath = FString() / FApp::GetProjectName() / TEXT("Content");
|
|
|
|
if (this != GetClass()->GetDefaultObject())
|
|
{
|
|
if (!UObject::IsGarbageEliminationEnabled())
|
|
{
|
|
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().AddUObject(this, &UOnlineHotfixManager::StopTrackingInvalidHotfixedAssets);
|
|
}
|
|
|
|
UE::DynamicConfig::HotfixPluginForBranch.AddUObject(this, &UOnlineHotfixManager::HotfixDynamicBranch);
|
|
}
|
|
}
|
|
|
|
UOnlineHotfixManager::UOnlineHotfixManager(FVTableHelper& Helper)
|
|
: Super(Helper)
|
|
{
|
|
}
|
|
|
|
UOnlineHotfixManager::~UOnlineHotfixManager()
|
|
{
|
|
if (!UObject::IsGarbageEliminationEnabled())
|
|
{
|
|
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().RemoveAll(this);
|
|
}
|
|
}
|
|
|
|
UOnlineHotfixManager* UOnlineHotfixManager::Get(UWorld* World)
|
|
{
|
|
UOnlineHotfixManager* DefaultObject = UOnlineHotfixManager::StaticClass()->GetDefaultObject<UOnlineHotfixManager>();
|
|
IOnlineSubsystem* OnlineSub = Online::GetSubsystem(World, DefaultObject->OSSName.Len() > 0 ? FName(*DefaultObject->OSSName) : NAME_None);
|
|
if (OnlineSub != nullptr)
|
|
{
|
|
UOnlineHotfixManager* HotfixManager = Cast<UOnlineHotfixManager>(OnlineSub->GetNamedInterface(NAME_HotfixManager));
|
|
if (HotfixManager == nullptr)
|
|
{
|
|
FString HotfixManagerClassName = DefaultObject->HotfixManagerClassName;
|
|
UClass* HotfixManagerClass = LoadClass<UOnlineHotfixManager>(nullptr, *HotfixManagerClassName, nullptr, LOAD_None, nullptr);
|
|
if (HotfixManagerClass == nullptr)
|
|
{
|
|
// Retrieve the static value, before any plugins, hotfixes, or any other dynamic configs have been applied, as a fallback
|
|
// This is not a standard method for retrieving config values, and should not be used as a general solution in other code
|
|
FString FallbackClassName;
|
|
GConfig->FindBranch(TEXT("Engine"), TEXT("DefaultEngine"))->CombinedStaticLayers.GetString(TEXT("/Script/Hotfix.OnlineHotfixManager"), TEXT("HotfixManagerClassName"), FallbackClassName);
|
|
if (FallbackClassName.Len())
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Unable to locate hotfix manager class '%s'. Trying '%s' instead."), *HotfixManagerClassName, *FallbackClassName)
|
|
HotfixManagerClassName = FallbackClassName;
|
|
HotfixManagerClass = LoadClass<UOnlineHotfixManager>(nullptr, *HotfixManagerClassName, nullptr, LOAD_None, nullptr);
|
|
}
|
|
if (HotfixManagerClass == nullptr)
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Failed to find requested hotfix manager class '%s'. Falling back to default."), *HotfixManagerClassName);
|
|
// Just use the default class if it couldn't load what was specified
|
|
HotfixManagerClass = UOnlineHotfixManager::StaticClass();
|
|
}
|
|
}
|
|
// Create it and store it
|
|
HotfixManager = NewObject<UOnlineHotfixManager>(GetTransientPackage(), HotfixManagerClass);
|
|
OnlineSub->SetNamedInterface(NAME_HotfixManager, HotfixManager);
|
|
}
|
|
|
|
if (World)
|
|
{
|
|
HotfixManager->OwnerWorld = World;
|
|
}
|
|
return HotfixManager;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void UOnlineHotfixManager::PostInitProperties()
|
|
{
|
|
#if !UE_BUILD_SHIPPING
|
|
FParse::Value(FCommandLine::Get(), TEXT("HOTFIXPREFIX="), DebugPrefix);
|
|
if (!DebugPrefix.IsEmpty() && !DebugPrefix.EndsWith(HOTFIX_SEPARATOR))
|
|
{
|
|
DebugPrefix += HOTFIX_SEPARATOR;
|
|
}
|
|
#endif
|
|
// So we only try to apply files for this platform
|
|
PlatformPrefix = DebugPrefix + ANSI_TO_TCHAR(FPlatformProperties::PlatformName());
|
|
PlatformPrefix += HOTFIX_SEPARATOR;
|
|
// Server prefix
|
|
ServerPrefix = DebugPrefix + GetDedicatedServerPrefix();
|
|
// Build the default prefix too
|
|
DefaultPrefix = DebugPrefix + TEXT("Default");
|
|
|
|
Super::PostInitProperties();
|
|
}
|
|
|
|
void UOnlineHotfixManager::Init()
|
|
{
|
|
bHotfixingInProgress = true;
|
|
bHotfixNeedsMapReload = false;
|
|
TotalFiles = 0;
|
|
NumDownloaded = 0;
|
|
TotalBytes = 0;
|
|
NumBytes = 0;
|
|
ChangedOrRemovedPakCount = 0;
|
|
OnlineTitleFile = Online::GetTitleFileInterface(OSSName.Len() ? FName(*OSSName, FNAME_Find) : NAME_None);
|
|
if (OnlineTitleFile.IsValid())
|
|
{
|
|
OnEnumerateFilesCompleteDelegateHandle = OnlineTitleFile->AddOnEnumerateFilesCompleteDelegate_Handle(OnEnumerateFilesCompleteDelegate);
|
|
OnReadFileProgressDelegateHandle = OnlineTitleFile->AddOnReadFileProgressDelegate_Handle(OnReadFileProgressDelegate);
|
|
OnReadFileCompleteDelegateHandle = OnlineTitleFile->AddOnReadFileCompleteDelegate_Handle(OnReadFileCompleteDelegate);
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::Cleanup()
|
|
{
|
|
PendingHotfixFiles.Empty();
|
|
if (OnlineTitleFile.IsValid())
|
|
{
|
|
// Make sure to give back the memory used when reading the hotfix files
|
|
OnlineTitleFile->ClearFiles();
|
|
OnlineTitleFile->ClearOnEnumerateFilesCompleteDelegate_Handle(OnEnumerateFilesCompleteDelegateHandle);
|
|
OnlineTitleFile->ClearOnReadFileProgressDelegate_Handle(OnReadFileProgressDelegateHandle);
|
|
OnlineTitleFile->ClearOnReadFileCompleteDelegate_Handle(OnReadFileCompleteDelegateHandle);
|
|
}
|
|
OnlineTitleFile = nullptr;
|
|
bHotfixingInProgress = false;
|
|
AsyncFlushContext = nullptr;
|
|
}
|
|
|
|
void UOnlineHotfixManager::StartHotfixProcess()
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Starting Hotfix Process"));
|
|
|
|
// Patching the editor this way seems like a bad idea
|
|
const bool bShouldHotfix = ShouldPerformHotfix();
|
|
if (!bShouldHotfix)
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Hotfixing skipped when not running game/server"));
|
|
TriggerHotfixComplete(EHotfixResult::SuccessNoChange);
|
|
return;
|
|
}
|
|
|
|
if (bHotfixingInProgress)
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Hotfixing already in progress"));
|
|
return;
|
|
}
|
|
|
|
Init();
|
|
// Kick off an enumeration of the files that are available to download
|
|
if (OnlineTitleFile.IsValid())
|
|
{
|
|
OnlineTitleFile->EnumerateFiles();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("Failed to start the hotfixing process due to no OnlineTitleInterface present for OSS(%s)"), *OSSName);
|
|
TriggerHotfixComplete(EHotfixResult::Failed);
|
|
}
|
|
}
|
|
|
|
struct FHotfixFileSortPredicate
|
|
{
|
|
struct FHotfixFileNameSortPredicate
|
|
{
|
|
const FString PlatformPrefix;
|
|
const FString ServerPrefix;
|
|
const FString DefaultPrefix;
|
|
|
|
FHotfixFileNameSortPredicate(const FString& InPlatformPrefix, const FString& InServerPrefix, const FString& InDefaultPrefix) :
|
|
PlatformPrefix(InPlatformPrefix),
|
|
ServerPrefix(InServerPrefix),
|
|
DefaultPrefix(InDefaultPrefix)
|
|
{
|
|
}
|
|
|
|
int32 GetPriorityForCompare(const FString& InHotfixName) const
|
|
{
|
|
// Non-ini files are applied last
|
|
int32 Priority = 50;
|
|
|
|
if (InHotfixName.EndsWith(TEXT("INI")))
|
|
{
|
|
FString HotfixName, NetworkVersion, BranchVersion;
|
|
GetFilenameAndVersion(InHotfixName, HotfixName, NetworkVersion, BranchVersion);
|
|
|
|
// Defaults are applied first
|
|
if (HotfixName.StartsWith(DefaultPrefix))
|
|
{
|
|
Priority = 10;
|
|
}
|
|
// Server trumps default
|
|
else if (HotfixName.StartsWith(ServerPrefix))
|
|
{
|
|
Priority = 20;
|
|
}
|
|
// Platform trumps server
|
|
else if (HotfixName.StartsWith(PlatformPrefix))
|
|
{
|
|
Priority = 30;
|
|
}
|
|
// Other INIs listed in game override of WantsHotfixProcessing will trump all other INIs
|
|
else
|
|
{
|
|
Priority = 40;
|
|
}
|
|
|
|
if (!BranchVersion.IsEmpty())
|
|
{
|
|
// Branch versioned hotfixes apply after all but net versioned hotfixes within their type
|
|
Priority += 3;
|
|
}
|
|
|
|
if (!NetworkVersion.IsEmpty())
|
|
{
|
|
// Network versioned hotfixes apply last within their type
|
|
Priority += 5;
|
|
}
|
|
}
|
|
|
|
return Priority;
|
|
}
|
|
|
|
bool Compare(const FString& A, const FString& B) const
|
|
{
|
|
int32 APriority = GetPriorityForCompare(A);
|
|
int32 BPriority = GetPriorityForCompare(B);
|
|
if (APriority != BPriority)
|
|
{
|
|
return APriority < BPriority;
|
|
}
|
|
else
|
|
{
|
|
// Fall back to sort by the string order if both have same priority
|
|
return A < B;
|
|
}
|
|
}
|
|
};
|
|
FHotfixFileNameSortPredicate FileNameSorter;
|
|
|
|
FHotfixFileSortPredicate(const FString& InPlatformPrefix, const FString& InServerPrefix, const FString& InDefaultPrefix) :
|
|
FileNameSorter(InPlatformPrefix, InServerPrefix, InDefaultPrefix)
|
|
{
|
|
}
|
|
|
|
bool operator()(const FCloudFileHeader &A, const FCloudFileHeader &B) const
|
|
{
|
|
return FileNameSorter.Compare(A.FileName, B.FileName);
|
|
}
|
|
|
|
bool operator()(const FString& A, const FString& B) const
|
|
{
|
|
return FileNameSorter.Compare(FPaths::GetCleanFilename(A), FPaths::GetCleanFilename(B));
|
|
}
|
|
};
|
|
|
|
void UOnlineHotfixManager::OnEnumerateFilesComplete(bool bWasSuccessful, const FString& ErrorStr)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("EnumerateFiles Http Request Complete"));
|
|
|
|
if (bWasSuccessful)
|
|
{
|
|
check(OnlineTitleFile.IsValid());
|
|
// Cache our current set so we can compare for differences
|
|
LastHotfixFileList = HotfixFileList;
|
|
HotfixFileList.Empty();
|
|
// Get the new header data
|
|
OnlineTitleFile->GetFileList(HotfixFileList);
|
|
FilterHotfixFiles();
|
|
// Reduce the set of work to just the files that changed since last run
|
|
BuildHotfixFileListDeltas();
|
|
// Sort after filtering so that the comparison below doesn't fail to different order from the server
|
|
ChangedHotfixFileList.Sort<FHotfixFileSortPredicate>(FHotfixFileSortPredicate(PlatformPrefix, ServerPrefix, DefaultPrefix));
|
|
// Read any changed files
|
|
if (ChangedHotfixFileList.Num() > 0)
|
|
{
|
|
// Update our totals for our progress delegates
|
|
TotalFiles = ChangedHotfixFileList.Num();
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
TotalBytes += FileHeader.FileSize;
|
|
}
|
|
ReadHotfixFiles();
|
|
}
|
|
else
|
|
{
|
|
if (RemovedHotfixFileList.Num() > 0)
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Files have been removed since last check. Reverting."));
|
|
|
|
// Prevent async loading while reverting hotfixes.
|
|
check(!AsyncFlushContext);
|
|
AsyncFlushContext = MakeUnique<FAsyncLoadingFlushContext>(TEXT("RevertHotfix"));
|
|
AsyncFlushContext->Flush(
|
|
FOnAsyncLoadingFlushComplete::CreateWeakLambda(
|
|
this,
|
|
[this]()
|
|
{
|
|
// No changes, just reverts
|
|
// Perform any undo operations needed
|
|
RestoreBackupIniFiles();
|
|
UnmountHotfixFiles();
|
|
|
|
for (const FCloudFileHeader& FileHeader : RemovedHotfixFileList)
|
|
{
|
|
TriggerOnHotfixRemovedFileDelegates(FileHeader.FileName);
|
|
}
|
|
|
|
TriggerHotfixComplete(EHotfixResult::SuccessNoChange);
|
|
}));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Returned hotfix data is the same as last application, skipping the apply phase"));
|
|
TriggerHotfixComplete(EHotfixResult::SuccessNoChange);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Enumeration of hotfix files failed"));
|
|
TriggerHotfixComplete(EHotfixResult::Failed);
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::CheckAvailability(FOnHotfixAvailableComplete& InCompletionDelegate)
|
|
{
|
|
// Checking for hotfixes in editor is not supported
|
|
const bool bShouldHotfix = ShouldPerformHotfix();
|
|
if (!bShouldHotfix)
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Hotfixing availability skipped when not running game/server"));
|
|
InCompletionDelegate.ExecuteIfBound(EHotfixResult::SuccessNoChange);
|
|
return;
|
|
}
|
|
|
|
if (bHotfixingInProgress)
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Hotfixing availability skipped because hotfix in progress"));
|
|
InCompletionDelegate.ExecuteIfBound(EHotfixResult::Failed);
|
|
return;
|
|
}
|
|
|
|
OnlineTitleFile = Online::GetTitleFileInterface(OSSName.Len() ? FName(*OSSName, FNAME_Find) : NAME_None);
|
|
|
|
bHotfixingInProgress = true;
|
|
|
|
// Kick off an enumeration of the files that are available to download
|
|
if (OnlineTitleFile.IsValid())
|
|
{
|
|
FOnEnumerateFilesCompleteDelegate OnEnumerateFilesForAvailabilityCompleteDelegate;
|
|
OnEnumerateFilesForAvailabilityCompleteDelegate.BindUObject(this, &UOnlineHotfixManager::OnEnumerateFilesForAvailabilityComplete, InCompletionDelegate);
|
|
OnEnumerateFilesForAvailabilityCompleteDelegateHandle = OnlineTitleFile->AddOnEnumerateFilesCompleteDelegate_Handle(OnEnumerateFilesForAvailabilityCompleteDelegate);
|
|
|
|
// The hotfix process isn't purely reversible as it kicks off HTTP requests, so we defer the work until transaction commit time.
|
|
UE_AUTORTFM_ONCOMMIT(this)
|
|
{
|
|
OnlineTitleFile->EnumerateFiles();
|
|
};
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("Failed to start the hotfix check process due to no OnlineTitleInterface present for OSS(%s)"), *OSSName);
|
|
TriggerHotfixComplete(EHotfixResult::Failed);
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::OnHotfixAvailablityCheck(const TArray<FCloudFileHeader>& PendingChangedFiles, const TArray<FCloudFileHeader>& PendingRemoveFiles)
|
|
{
|
|
// empty in base class
|
|
}
|
|
|
|
void UOnlineHotfixManager::OnEnumerateFilesForAvailabilityComplete(bool bWasSuccessful, const FString& ErrorStr, FOnHotfixAvailableComplete InCompletionDelegate)
|
|
{
|
|
if (OnlineTitleFile.IsValid())
|
|
{
|
|
OnlineTitleFile->ClearOnEnumerateFilesCompleteDelegate_Handle(OnEnumerateFilesForAvailabilityCompleteDelegateHandle);
|
|
}
|
|
|
|
EHotfixResult Result = EHotfixResult::Failed;
|
|
if (bWasSuccessful)
|
|
{
|
|
TArray<FCloudFileHeader> TmpHotfixFileList;
|
|
TArray<FCloudFileHeader> TmpLastHotfixFileList;
|
|
|
|
TmpHotfixFileList = HotfixFileList;
|
|
TmpLastHotfixFileList = LastHotfixFileList;
|
|
|
|
// Cache our current set so we can compare for differences
|
|
LastHotfixFileList = HotfixFileList;
|
|
HotfixFileList.Empty();
|
|
// Get the new header data
|
|
OnlineTitleFile->GetFileList(HotfixFileList);
|
|
FilterHotfixFiles();
|
|
// Reduce the set of work to just the files that changed since last run
|
|
BuildHotfixFileListDeltas();
|
|
|
|
// Read any changed files
|
|
if (ChangedHotfixFileList.Num() > 0 || RemovedHotfixFileList.Num() > 0)
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Hotfix files available"));
|
|
Result = EHotfixResult::Success;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Returned hotfix data is the same as last application, returning nothing to do"));
|
|
Result = EHotfixResult::SuccessNoChange;
|
|
}
|
|
|
|
OnHotfixAvailablityCheck(ChangedHotfixFileList, RemovedHotfixFileList);
|
|
|
|
// Restore state to before the check
|
|
RemovedHotfixFileList.Empty();
|
|
ChangedHotfixFileList.Empty();
|
|
HotfixFileList = TmpHotfixFileList;
|
|
LastHotfixFileList = TmpLastHotfixFileList;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Enumeration of hotfix files failed"));
|
|
}
|
|
|
|
OnlineTitleFile = nullptr;
|
|
bHotfixingInProgress = false;
|
|
InCompletionDelegate.ExecuteIfBound(Result);
|
|
}
|
|
|
|
void UOnlineHotfixManager::BuildHotfixFileListDeltas()
|
|
{
|
|
RemovedHotfixFileList.Empty();
|
|
ChangedHotfixFileList.Empty();
|
|
// Go through the current list and see if it's changed from the previous attempt
|
|
TSet<FString> DirtyIniCategories;
|
|
for (const FCloudFileHeader& CurrentHeader : HotfixFileList)
|
|
{
|
|
bool bFoundMatch = LastHotfixFileList.Contains(CurrentHeader);
|
|
if (!bFoundMatch)
|
|
{
|
|
// All NEW or CHANGED ini files will be added to the process list
|
|
ChangedHotfixFileList.Add(CurrentHeader);
|
|
|
|
if (CurrentHeader.FileName.EndsWith(TEXT(".INI"), ESearchCase::IgnoreCase))
|
|
{
|
|
// Make sure that ALL INIs of this "category" get marked for inclusion below
|
|
DirtyIniCategories.Add(GetStrippedConfigFileName(CurrentHeader.FileName));
|
|
}
|
|
}
|
|
}
|
|
// Find any files that have been removed from the set of hotfix files
|
|
for (const FCloudFileHeader& LastHeader : LastHotfixFileList)
|
|
{
|
|
bool bFoundMatch = HotfixFileList.ContainsByPredicate(
|
|
[&LastHeader](const FCloudFileHeader& CurrentHeader)
|
|
{
|
|
return LastHeader.FileName == CurrentHeader.FileName;
|
|
});
|
|
if (!bFoundMatch)
|
|
{
|
|
// We've been removed so add to the removed list
|
|
RemovedHotfixFileList.Add(LastHeader);
|
|
|
|
if (LastHeader.FileName.EndsWith(TEXT(".INI"), ESearchCase::IgnoreCase))
|
|
{
|
|
// Make sure that ALL INIs of this "category" get marked for inclusion below
|
|
DirtyIniCategories.Add(GetStrippedConfigFileName(LastHeader.FileName));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply all hotfix files for each ini file if the category has been marked dirty
|
|
// For example, if DefaultGame.ini has changed, also consider XboxOne_Game.ini changed
|
|
// This is necessary because we revert the ini file to the pre-hotfix state
|
|
if (DirtyIniCategories.Num() > 0)
|
|
{
|
|
for (const FCloudFileHeader& CurrentHeader : HotfixFileList)
|
|
{
|
|
if (CurrentHeader.FileName.EndsWith(TEXT(".INI"), ESearchCase::IgnoreCase))
|
|
{
|
|
for (const FString& StrippedIniName : DirtyIniCategories)
|
|
{
|
|
if (CurrentHeader.FileName.EndsWith(StrippedIniName, ESearchCase::IgnoreCase))
|
|
{
|
|
// Be sure to include any ini in a "dirty" category that remains in the latest HotfixFileList
|
|
ChangedHotfixFileList.AddUnique(CurrentHeader);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix deltas: updated:[%s], removed:[%s]"),
|
|
*FString::JoinBy(ChangedHotfixFileList, TEXT(", "), &FCloudFileHeader::FileName),
|
|
*FString::JoinBy(RemovedHotfixFileList, TEXT(", "), &FCloudFileHeader::FileName));
|
|
}
|
|
|
|
void UOnlineHotfixManager::FilterHotfixFiles()
|
|
{
|
|
for (int32 Idx = 0; Idx < HotfixFileList.Num(); Idx++)
|
|
{
|
|
if (!WantsHotfixProcessing(HotfixFileList[Idx]))
|
|
{
|
|
HotfixFileList.RemoveAt(Idx, EAllowShrinking::No);
|
|
Idx--;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::ReadHotfixFiles()
|
|
{
|
|
if (ChangedHotfixFileList.Num())
|
|
{
|
|
check(OnlineTitleFile.IsValid());
|
|
// Kick off a read for each file
|
|
// Do this in two passes so already cached files don't trigger completion
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("HF: %s %s %d "), *FileHeader.DLName, *FileHeader.FileName, FileHeader.FileSize);
|
|
PendingHotfixFiles.Add(FileHeader.DLName, FPendingFileDLProgress());
|
|
}
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
OnlineTitleFile->ReadFile(FileHeader.DLName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("No hotfix files need to be downloaded"));
|
|
TriggerHotfixComplete(EHotfixResult::Success);
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::OnReadFileComplete(bool bWasSuccessful, const FString& FileName)
|
|
{
|
|
if (PendingHotfixFiles.Contains(FileName))
|
|
{
|
|
if (bWasSuccessful)
|
|
{
|
|
FCloudFileHeader* Header = GetFileHeaderFromDLName(FileName);
|
|
check(Header != nullptr);
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix file (%s) downloaded. Size was (%d)"), *GetFriendlyNameFromDLName(FileName), Header->FileSize);
|
|
// Completion updates the file count and progress updates the byte count
|
|
UpdateProgress(1, 0);
|
|
PendingHotfixFiles.Remove(FileName);
|
|
if (PendingHotfixFiles.Num() == 0)
|
|
{
|
|
// Prevent async loading while applying hotfixes.
|
|
check(!AsyncFlushContext);
|
|
AsyncFlushContext = MakeUnique<FAsyncLoadingFlushContext>(TEXT("ApplyHotfix"));
|
|
AsyncFlushContext->Flush(
|
|
FOnAsyncLoadingFlushComplete::CreateWeakLambda(
|
|
this,
|
|
[this]()
|
|
{
|
|
const EHotfixResult Result = ApplyHotfix();
|
|
TriggerHotfixComplete(Result);
|
|
}));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("Hotfix file (%s) failed to download"), *GetFriendlyNameFromDLName(FileName));
|
|
TriggerHotfixComplete(EHotfixResult::Failed);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::UpdateProgress(uint32 FileCount, uint64 UpdateSize)
|
|
{
|
|
NumDownloaded += FileCount;
|
|
NumBytes += UpdateSize;
|
|
// Update our progress
|
|
TriggerOnHotfixProgressDelegates(NumDownloaded, TotalFiles, NumBytes, TotalBytes);
|
|
}
|
|
|
|
EHotfixResult UOnlineHotfixManager::ApplyHotfix()
|
|
{
|
|
// Check the CVar value before unmounting any previous hotfixes in case the value was changed by the previous hotfix.
|
|
const bool bEmptyDynamicHotfixContentsOnNewHotfix = CVarEmptyDynamicHotfixContentsOnNewHotfix->GetBool();
|
|
if (bEmptyDynamicHotfixContentsOnNewHotfix)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("[DynamicHotfixContents] Removing ini data for all pending hotfixes"));
|
|
DynamicHotfixContents.Empty();
|
|
}
|
|
|
|
// Perform any undo operations needed
|
|
// This occurs same frame as the application of new hotfixes
|
|
RestoreBackupIniFiles();
|
|
UnmountHotfixFiles();
|
|
|
|
for (const FCloudFileHeader& FileHeader : RemovedHotfixFileList)
|
|
{
|
|
TriggerOnHotfixRemovedFileDelegates(FileHeader.FileName);
|
|
|
|
if (!bEmptyDynamicHotfixContentsOnNewHotfix)
|
|
{
|
|
const FString HotfixTag = GetStrippedConfigFileName(FileHeader.FileName).Replace(TEXT(".ini"), TEXT(""));
|
|
UE_LOG(LogHotfixManager, Log, TEXT("[DynamicHotfixContents] Removing ini data for pending hotfix %s (%s)"), *FileHeader.FileName, *HotfixTag);
|
|
DynamicHotfixContents.Remove(FName(HotfixTag));
|
|
}
|
|
}
|
|
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
if (!ApplyHotfixProcessing(FileHeader))
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("Couldn't apply hotfix file (%s)"), *FileHeader.FileName);
|
|
return EHotfixResult::Failed;
|
|
}
|
|
// Let anyone listening know we just processed this file
|
|
TriggerOnHotfixProcessedFileDelegates(FileHeader.FileName, GetCachedDirectory() / FileHeader.DLName);
|
|
}
|
|
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Hotfix data has been successfully applied"));
|
|
EHotfixResult Result = EHotfixResult::Success;
|
|
if (ChangedOrRemovedPakCount > 0)
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Hotfix has changed or removed PAK files so a relaunch of the app is needed"));
|
|
Result = EHotfixResult::SuccessNeedsRelaunch;
|
|
}
|
|
else if (bHotfixNeedsMapReload)
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Hotfix has detected PAK files containing currently loaded maps, so a level load is needed"));
|
|
Result = EHotfixResult::SuccessNeedsReload;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
void UOnlineHotfixManager::ApplyLocalTestHotfix(FString Filename)
|
|
{
|
|
const FString CleanName = FPaths::GetCleanFilename(Filename);
|
|
FString IniData;
|
|
if (FFileHelper::LoadFileToString(IniData, *Filename))
|
|
{
|
|
if (HotfixIniFile(CleanName, IniData))
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Successfully applied test hotfix file %s"), *Filename);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Failed to apply test hotfix file %s"), *Filename);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Unable to read test hotfix file '%s'"), *Filename);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void UOnlineHotfixManager::TriggerHotfixComplete(EHotfixResult HotfixResult)
|
|
{
|
|
#if !UE_BUILD_SHIPPING
|
|
// Apply this here so it overwrites any downloaded hotfix changes
|
|
FString IniFilename;
|
|
if (FParse::Value(FCommandLine::Get(), TEXT("-TestHotfixIniFile="), IniFilename))
|
|
{
|
|
ApplyLocalTestHotfix(IniFilename);
|
|
}
|
|
#endif
|
|
|
|
if (HotfixResult != EHotfixResult::Failed && HotfixResult != EHotfixResult::SuccessNoChange)
|
|
{
|
|
PatchAssetsFromIniFiles();
|
|
}
|
|
|
|
TriggerOnHotfixCompleteDelegates(HotfixResult);
|
|
if (HotfixResult == EHotfixResult::Failed)
|
|
{
|
|
HotfixFileList.Empty();
|
|
UnmountHotfixFiles();
|
|
}
|
|
Cleanup();
|
|
}
|
|
|
|
bool UOnlineHotfixManager::WantsHotfixProcessing(const FCloudFileHeader& FileHeader)
|
|
{
|
|
const FString Extension = FPaths::GetExtension(FileHeader.FileName);
|
|
if (Extension == TEXT("INI"))
|
|
{
|
|
FString CloudFilename;
|
|
if (IsCompatibleHotfixFile(FileHeader.FileName, CloudFilename))
|
|
{
|
|
bool bIsServerHotfix = CloudFilename.StartsWith(ServerPrefix);
|
|
bool bWantsServerHotfix = IsRunningDedicatedServer() && bIsServerHotfix;
|
|
bool bWantsDefaultHotfix = CloudFilename.StartsWith(DefaultPrefix);
|
|
bool bWantsPlatformHotfix = CloudFilename.StartsWith(PlatformPrefix);
|
|
|
|
if (bWantsPlatformHotfix)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Using platform hotfix %s"), *FileHeader.FileName);
|
|
}
|
|
else if (bWantsServerHotfix)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Using server hotfix %s"), *FileHeader.FileName);
|
|
}
|
|
else if (bWantsDefaultHotfix)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Using default hotfix %s"), *FileHeader.FileName);
|
|
}
|
|
|
|
return bWantsPlatformHotfix || bWantsServerHotfix || bWantsDefaultHotfix;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Verbose, TEXT("File not compatible %s, skipping."), *FileHeader.FileName);
|
|
return false;
|
|
}
|
|
}
|
|
else if (Extension == TEXT("PAK"))
|
|
{
|
|
return FileHeader.FileName.Find(PlatformPrefix) != -1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UOnlineHotfixManager::ApplyHotfixProcessing(const FCloudFileHeader& FileHeader)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_UOnlineHotfixManager_ApplyHotfixProcessing);
|
|
|
|
bool bSuccess = false;
|
|
const FString Extension = FPaths::GetExtension(FileHeader.FileName);
|
|
if (Extension == TEXT("PAK"))
|
|
{
|
|
bSuccess = HotfixPakFile(FileHeader);
|
|
}
|
|
else
|
|
{
|
|
TArray<uint8> FileData;
|
|
if (OnlineTitleFile->GetFileContents(FileHeader.DLName, FileData))
|
|
{
|
|
if (PreProcessDownloadedFileData(FileHeader, FileData))
|
|
{
|
|
TriggerOnHotfixUpdatedFileDelegates(FileHeader.FileName, FileData);
|
|
|
|
if (Extension == TEXT("INI"))
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Applying hotfix %s"), *FileHeader.FileName);
|
|
|
|
// Convert to a FString
|
|
FileData.Add(0);
|
|
FString HotfixStr;
|
|
FFileHelper::BufferToString(HotfixStr, FileData.GetData(), FileData.Num());
|
|
bSuccess = HotfixIniFile(FileHeader.FileName, HotfixStr);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Failed to process contents of %s"), *FileHeader.FileName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Failed to get contents of %s"), *FileHeader.FileName);
|
|
}
|
|
}
|
|
OnlineTitleFile->ClearFile(FileHeader.FileName);
|
|
return bSuccess;
|
|
}
|
|
|
|
FString UOnlineHotfixManager::GetStrippedConfigFileName(const FString& IniName)
|
|
{
|
|
FString StrippedIniName;
|
|
FString NetworkVersion;
|
|
FString BranchVersion;
|
|
GetFilenameAndVersion(IniName, StrippedIniName, NetworkVersion, BranchVersion);
|
|
|
|
if (StrippedIniName.StartsWith(PlatformPrefix))
|
|
{
|
|
StrippedIniName = IniName.Right(StrippedIniName.Len() - PlatformPrefix.Len());
|
|
}
|
|
else if (StrippedIniName.StartsWith(ServerPrefix))
|
|
{
|
|
StrippedIniName = IniName.Right(StrippedIniName.Len() - ServerPrefix.Len());
|
|
}
|
|
else if (StrippedIniName.StartsWith(DefaultPrefix))
|
|
{
|
|
StrippedIniName = IniName.Right(StrippedIniName.Len() - DefaultPrefix.Len());
|
|
}
|
|
else if (StrippedIniName.StartsWith(DebugPrefix))
|
|
{
|
|
StrippedIniName = IniName.Right(StrippedIniName.Len() - DebugPrefix.Len());
|
|
}
|
|
return StrippedIniName;
|
|
}
|
|
|
|
FString UOnlineHotfixManager::BuildConfigCacheKey(const FString& IniName)
|
|
{
|
|
const FString IniNameNoExtension = FPaths::GetBaseFilename(IniName);
|
|
|
|
return GConfig->GetConfigFilename(*IniNameNoExtension);
|
|
}
|
|
|
|
FConfigBranch* UOnlineHotfixManager::GetBranch(const FString& IniName)
|
|
{
|
|
const FString StrippedIniName(GetStrippedConfigFileName(IniName));
|
|
const FString StrippedIniNameNoExtension = FPaths::GetBaseFilename(StrippedIniName);
|
|
|
|
// find the branch by basename or full filename
|
|
FConfigBranch* Branch = GConfig->FindBranch(*StrippedIniNameNoExtension, StrippedIniNameNoExtension);
|
|
|
|
return Branch;
|
|
}
|
|
|
|
FConfigFile* UOnlineHotfixManager::GetConfigFile(const FString& IniName)
|
|
{
|
|
const FString StrippedIniName(GetStrippedConfigFileName(IniName));
|
|
const FString StrippedIniNameNoExtension = FPaths::GetBaseFilename(StrippedIniName);
|
|
|
|
FConfigFile* ConfigFile = nullptr;
|
|
|
|
// Start by searching for a known config name.
|
|
if (GConfig->IsKnownConfigName(FName(*StrippedIniNameNoExtension, FNAME_Find)))
|
|
{
|
|
ConfigFile = GConfig->FindConfigFile(StrippedIniNameNoExtension);
|
|
}
|
|
|
|
// Fall back to a partial path search.
|
|
if (ConfigFile == nullptr)
|
|
{
|
|
// Look for the first matching INI file entry
|
|
for (const FString& IniFilename : GConfig->GetFilenames())
|
|
{
|
|
if (IniFilename.EndsWith(StrippedIniName) || IniFilename == StrippedIniNameNoExtension)
|
|
{
|
|
ConfigFile = GConfig->FindConfigFile(IniFilename);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not found, add this file to the config cache.
|
|
if (ConfigFile == nullptr)
|
|
{
|
|
const FString ProcessedName(BuildConfigCacheKey(StrippedIniName));
|
|
FConfigFile Empty;
|
|
GConfig->SetFile(ProcessedName, &Empty);
|
|
ConfigFile = GConfig->Find(ProcessedName);
|
|
}
|
|
check(ConfigFile);
|
|
// We never want to save these merged files
|
|
ConfigFile->NoSave = true;
|
|
return ConfigFile;
|
|
}
|
|
|
|
bool UOnlineHotfixManager::HotfixIniFile(const FString& FileName, const FString& IniData)
|
|
{
|
|
// Flush async loading before modifying GConfig.
|
|
FlushAsyncLoading();
|
|
|
|
static bool bUseNewDynamicLayers = CVarUseNewDynamicLayersForHotfix->GetInt() != 0;
|
|
if (bUseNewDynamicLayers)
|
|
{
|
|
FName Tag = *BuildConfigCacheKey(FileName);
|
|
UE::DynamicConfig::PerformDynamicConfig(Tag, [this, Tag, FileName, IniData](FConfigModificationTracker* ChangeTracker)
|
|
{
|
|
FConfigBranch* Branch = GetBranch(FileName);
|
|
if (Branch)
|
|
{
|
|
ChangeTracker->CVars.Add(TEXT("ConsoleVariables")).CVarPriority = (int)ECVF_SetByHotfix;
|
|
Branch->AddDynamicLayerStringToHierarchy(FileName, IniData, Tag, DynamicLayerPriority::Hotfix, ChangeTracker);
|
|
}
|
|
else
|
|
{
|
|
const FString HotfixTag = GetStrippedConfigFileName(FileName).Replace(TEXT(".ini"), TEXT(""));
|
|
|
|
UE_LOG(LogHotfixManager, Log, TEXT("[DynamicHotfixContents] Storing ini data for pending hotfix %s (%s)"), *FileName, *HotfixTag);
|
|
|
|
TArray<TPair<FString, FString>>& HotfixesForTag = DynamicHotfixContents.FindOrAdd(*HotfixTag);
|
|
HotfixesForTag.Add(TPair<FString, FString>(FileName, IniData));
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
FConfigFile* ConfigFile = GetConfigFile(FileName);
|
|
// Store the original file so we can undo this later
|
|
FConfigFileBackup& BackupFile = BackupIniFile(FileName, ConfigFile);
|
|
// Merge the string into the config file
|
|
ConfigFile->CombineFromBuffer(IniData, FileName);
|
|
|
|
ReloadObjectsAffectedByConfigFile(FileName, IniData, ConfigFile->Name.ToString(), BackupFile.ClassesReloaded, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UOnlineHotfixManager::HotfixPakFile(const FCloudFileHeader& FileHeader)
|
|
{
|
|
if (!FCoreDelegates::MountPak.IsBound())
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("PAK file (%s) could not be mounted because MountPak is not bound"), *FileHeader.FileName);
|
|
return false;
|
|
}
|
|
FString PakLocation = FString::Printf(TEXT("%s/%s"), *GetCachedDirectory(), *FileHeader.DLName);
|
|
if (IPakFile* PakFile = FCoreDelegates::MountPak.Execute(PakLocation, 0))
|
|
{
|
|
MountedPakFiles.Add(FileHeader.DLName);
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix mounted PAK file (%s)"), *FileHeader.FileName);
|
|
int32 NumInisReloaded = 0;
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
// Iterate through the the pak file's contents for INI and asset reloading.
|
|
TArray<FString> IniList;
|
|
FPakFileVisitor Visitor;
|
|
PakFile->PakVisitPrunedFilenames(Visitor);
|
|
for (const FString& InternalPakFileName : Visitor.Files)
|
|
{
|
|
if (InternalPakFileName.EndsWith(TEXT(".ini")))
|
|
{
|
|
IniList.Add(InternalPakFileName);
|
|
}
|
|
}
|
|
|
|
// Iterate through all loaded maps and see if they have patches in the pak file and therefore this hotfix needs to reload a map
|
|
for (TObjectIterator<UPackage> it; !bHotfixNeedsMapReload && it; ++it)
|
|
{
|
|
UPackage* Package = *it;
|
|
if (Package && Package->ContainsMap())
|
|
{
|
|
const FString FileName = FPackageName::LongPackageNameToFilename(Package->GetLoadedPath().GetPackageName(), FPackageName::GetMapPackageExtension());
|
|
if (PakFile->PakContains(FileName))
|
|
{
|
|
bHotfixNeedsMapReload = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort the INIs so they are processed consistently
|
|
IniList.Sort<FHotfixFileSortPredicate>(FHotfixFileSortPredicate(PlatformPrefix, ServerPrefix, DefaultPrefix));
|
|
// Now process the INIs in sorted order
|
|
for (const FString& IniName : IniList)
|
|
{
|
|
HotfixPakIniFile(IniName);
|
|
NumInisReloaded++;
|
|
}
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Processing pak file (%s) took %f seconds and resulted in (%d) INIs being reloaded"),
|
|
*FileHeader.FileName, FPlatformTime::Seconds() - StartTime, NumInisReloaded);
|
|
#if !UE_BUILD_SHIPPING
|
|
if (bLogMountedPakContents)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Files in pak file (%s):"), *FileHeader.FileName);
|
|
for (const FString& FileName : Visitor.Files)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("\t\t%s"), *FileName);
|
|
}
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UOnlineHotfixManager::IsMapLoaded(const FString& MapName)
|
|
{
|
|
FString MapPackageName(MapName.Left(MapName.Len() - 5));
|
|
MapPackageName = MapPackageName.Replace(*GameContentPath, TEXT("/Game"));
|
|
// If this map's UPackage exists, it is currently loaded
|
|
UPackage* MapPackage = FindObject<UPackage>(nullptr, *MapPackageName, true);
|
|
return MapPackage != nullptr;
|
|
}
|
|
|
|
bool UOnlineHotfixManager::HotfixPakIniFile(const FString& FileName)
|
|
{
|
|
// Flush async loading before modifying GConfig.
|
|
FlushAsyncLoading();
|
|
|
|
FString StrippedName;
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
// Need to strip off the PAK path
|
|
FileName.Split(TEXT("/"), nullptr, &StrippedName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
|
|
FConfigFile* ConfigFile = GetConfigFile(StrippedName);
|
|
if (!ConfigFile->Combine(FString(TEXT("../../../")) + FileName.Replace(*GameContentPath, TEXT("/Game"))))
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix failed to merge INI (%s) found in a PAK file"), *FileName);
|
|
return false;
|
|
}
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix merged INI (%s) found in a PAK file"), *FileName);
|
|
int32 NumObjectsReloaded = 0;
|
|
// Now that we have a list of classes to update, we can iterate objects and
|
|
// reload if they match the INI file that was changed
|
|
TArray<UObject*> Classes;
|
|
GetObjectsOfClass(UClass::StaticClass(), Classes, true, RF_NoFlags);
|
|
TArray<UClass*> ClassesToReload;
|
|
for (UObject* ClassObject : Classes)
|
|
{
|
|
if (UClass* const Class = Cast<UClass>(ClassObject))
|
|
{
|
|
if (Class->HasAnyClassFlags(CLASS_Config) &&
|
|
Class->ClassConfigName == ConfigFile->Name)
|
|
{
|
|
TArray<UObject*> Objects;
|
|
GetObjectsOfClass(Class, Objects, true, RF_NoFlags);
|
|
for (UObject* Object : Objects)
|
|
{
|
|
if (IsValid(Object))
|
|
{
|
|
// Force a reload of the config vars
|
|
Object->ReloadConfig();
|
|
NumObjectsReloaded++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Updating config from %s took %f seconds reloading %d objects"),
|
|
*FileName, FPlatformTime::Seconds() - StartTime, NumObjectsReloaded);
|
|
return true;
|
|
}
|
|
|
|
const FString UOnlineHotfixManager::GetFriendlyNameFromDLName(const FString& DLName) const
|
|
{
|
|
for (const FCloudFileHeader& Header : HotfixFileList)
|
|
{
|
|
if (Header.DLName == DLName)
|
|
{
|
|
return Header.FileName;
|
|
}
|
|
}
|
|
return FString();
|
|
}
|
|
|
|
void UOnlineHotfixManager::UnmountHotfixFiles()
|
|
{
|
|
if (MountedPakFiles.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
// Unmount any changed hotfix files since we need to download them again
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
for (int32 Index = 0; Index < MountedPakFiles.Num(); Index++)
|
|
{
|
|
if (MountedPakFiles[Index] == FileHeader.DLName)
|
|
{
|
|
FCoreDelegates::OnUnmountPak.Execute(MountedPakFiles[Index]);
|
|
MountedPakFiles.RemoveAt(Index);
|
|
ChangedOrRemovedPakCount++;
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix unmounted PAK file (%s) so it can be redownloaded"), *FileHeader.FileName);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Unmount any removed hotfix files
|
|
for (const FCloudFileHeader& FileHeader : RemovedHotfixFileList)
|
|
{
|
|
for (int32 Index = 0; Index < MountedPakFiles.Num(); Index++)
|
|
{
|
|
if (MountedPakFiles[Index] == FileHeader.DLName)
|
|
{
|
|
FCoreDelegates::OnUnmountPak.Execute(MountedPakFiles[Index]);
|
|
MountedPakFiles.RemoveAt(Index);
|
|
ChangedOrRemovedPakCount++;
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfix unmounted PAK file (%s) since it was removed from the hotfix set"), *FileHeader.FileName);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FCloudFileHeader* UOnlineHotfixManager::GetFileHeaderFromDLName(const FString& FileName)
|
|
{
|
|
for (int32 Index = 0; Index < HotfixFileList.Num(); Index++)
|
|
{
|
|
if (HotfixFileList[Index].DLName == FileName)
|
|
{
|
|
return &HotfixFileList[Index];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void UOnlineHotfixManager::OnReadFileProgress(const FString& FileName, uint64 BytesRead)
|
|
{
|
|
if (PendingHotfixFiles.Contains(FileName))
|
|
{
|
|
// Since the title file is reporting absolute numbers subtract out the last update so we can add a delta
|
|
uint64 Delta = BytesRead - PendingHotfixFiles[FileName].Progress;
|
|
PendingHotfixFiles[FileName].Progress = BytesRead;
|
|
// Completion updates the file count and progress updates the byte count
|
|
UpdateProgress(0, Delta);
|
|
}
|
|
}
|
|
|
|
UOnlineHotfixManager::FConfigFileBackup& UOnlineHotfixManager::BackupIniFile(const FString& IniName, const FConfigFile* ConfigFile)
|
|
{
|
|
FString BackupIniName = BuildConfigCacheKey(GetStrippedConfigFileName(IniName));
|
|
if (FConfigFileBackup* Backup = IniBackups.FindByPredicate([&BackupIniName](const FConfigFileBackup& Entry) { return Entry.IniName == BackupIniName; }))
|
|
{
|
|
// Only store one copy of each ini file, consisting of the original state
|
|
return *Backup;
|
|
}
|
|
|
|
int32 AddAt = IniBackups.AddDefaulted();
|
|
FConfigFileBackup& NewBackup = IniBackups[AddAt];
|
|
NewBackup.IniName = BackupIniName;
|
|
NewBackup.ConfigData = *ConfigFile;
|
|
return NewBackup;
|
|
}
|
|
|
|
void UOnlineHotfixManager::RestoreBackupIniFiles()
|
|
{
|
|
static bool bUseNewDynamicLayers = CVarUseNewDynamicLayersForHotfix->GetInt() != 0;
|
|
if (bUseNewDynamicLayers)
|
|
{
|
|
// @todo branch - would be nice to have a way to know nothing was backed up yet, with Branch mode, so we can skip the FlushAsyncLoading call when there's nothing to do
|
|
// Flush async loading before modifying GConfig.
|
|
FlushAsyncLoading();
|
|
|
|
// when just unloading, we don't need an actual tag
|
|
UE::DynamicConfig::PerformDynamicConfig(NAME_None, [this](FConfigModificationTracker* ChangeTracker)
|
|
{
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
if (FileHeader.FileName.EndsWith(TEXT(".INI")))
|
|
{
|
|
FName Tag = *BuildConfigCacheKey(FileHeader.FileName);
|
|
FConfigCacheIni::RemoveTagFromAllBranches(Tag, ChangeTracker);
|
|
}
|
|
}
|
|
for (const FCloudFileHeader& FileHeader : RemovedHotfixFileList)
|
|
{
|
|
if (FileHeader.FileName.EndsWith(TEXT(".INI")))
|
|
{
|
|
FName Tag = *BuildConfigCacheKey(FileHeader.FileName);
|
|
FConfigCacheIni::RemoveTagFromAllBranches(Tag, ChangeTracker);
|
|
}
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (IniBackups.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Flush async loading before modifying GConfig.
|
|
FlushAsyncLoading();
|
|
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
TArray<FString> ClassesToRestore;
|
|
|
|
// Restore any changed INI files and build a list of which ones changed for UObject reloading below
|
|
for (const FCloudFileHeader& FileHeader : ChangedHotfixFileList)
|
|
{
|
|
if (FileHeader.FileName.EndsWith(TEXT(".INI")))
|
|
{
|
|
const FString ProcessedName = BuildConfigCacheKey(GetStrippedConfigFileName(FileHeader.FileName));
|
|
for (int32 Index = 0; Index < IniBackups.Num(); Index++)
|
|
{
|
|
const FConfigFileBackup& BackupFile = IniBackups[Index];
|
|
if (IniBackups[Index].IniName == ProcessedName)
|
|
{
|
|
ClassesToRestore.Append(BackupFile.ClassesReloaded);
|
|
|
|
GConfig->SetFile(BackupFile.IniName, &BackupFile.ConfigData);
|
|
IniBackups.RemoveAt(Index);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also restore any files that were previously part of the hotfix and now are not
|
|
for (const FCloudFileHeader& FileHeader : RemovedHotfixFileList)
|
|
{
|
|
if (FileHeader.FileName.EndsWith(TEXT(".INI")))
|
|
{
|
|
const FString ProcessedName = BuildConfigCacheKey(GetStrippedConfigFileName(FileHeader.FileName));
|
|
for (int32 Index = 0; Index < IniBackups.Num(); Index++)
|
|
{
|
|
const FConfigFileBackup& BackupFile = IniBackups[Index];
|
|
if (BackupFile.IniName == ProcessedName)
|
|
{
|
|
ClassesToRestore.Append(BackupFile.ClassesReloaded);
|
|
|
|
GConfig->SetFile(BackupFile.IniName, &BackupFile.ConfigData);
|
|
IniBackups.RemoveAt(Index);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32 NumObjectsReloaded = 0;
|
|
if (ClassesToRestore.Num() > 0)
|
|
{
|
|
TArray<UClass*> RestoredClasses;
|
|
RestoredClasses.Reserve(ClassesToRestore.Num());
|
|
for (int32 Index = 0; Index < ClassesToRestore.Num(); Index++)
|
|
{
|
|
UClass* Class = FindObject<UClass>(nullptr, *ClassesToRestore[Index], true);
|
|
if (Class != nullptr)
|
|
{
|
|
// Add this to the list to check against
|
|
RestoredClasses.Add(Class);
|
|
}
|
|
}
|
|
|
|
for (UClass* Class : RestoredClasses)
|
|
{
|
|
if (Class->HasAnyClassFlags(CLASS_Config))
|
|
{
|
|
TArray<UObject*> Objects;
|
|
GetObjectsOfClass(Class, Objects, true, RF_NoFlags);
|
|
for (UObject* Object : Objects)
|
|
{
|
|
if (IsValid(Object))
|
|
{
|
|
UE_LOG(LogHotfixManager, Verbose, TEXT("Restoring %s"), *Object->GetPathName());
|
|
Object->ReloadConfig();
|
|
NumObjectsReloaded++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Restoring config for %d changed classes took %f seconds reloading %d objects"),
|
|
ClassesToRestore.Num(), FPlatformTime::Seconds() - StartTime, NumObjectsReloaded);
|
|
}
|
|
|
|
void UOnlineHotfixManager::PatchAssetsFromIniFiles()
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Checking for assets to be patched using data from 'AssetHotfix' section in the Game .ini file"));
|
|
|
|
int32 TotalPatchableAssets = 0;
|
|
AssetsHotfixedFromIniFiles.Reset();
|
|
|
|
// Everything should be under the 'AssetHotfix' section in Game.ini
|
|
const FConfigSection* AssetHotfixConfigSection = GConfig->GetSection(TEXT("AssetHotfix"), false, GGameIni);
|
|
if (AssetHotfixConfigSection != nullptr)
|
|
{
|
|
// Flush async loading before modifying GConfig.
|
|
FlushAsyncLoading();
|
|
|
|
// These are the asset types we support patching right now
|
|
UClass* const PatchableAssetClasses[] =
|
|
{
|
|
UCurveTable::StaticClass(),
|
|
UDataTable::StaticClass(),
|
|
UCurveFloat::StaticClass(),
|
|
UCurveVector::StaticClass(),
|
|
UCurveLinearColor::StaticClass(),
|
|
};
|
|
|
|
TSet<UDataTable*> ChangedDataTables;
|
|
TSet<UCurveTable*> ChangedCurveTables;
|
|
TSet<UCurveTable*>* ChangedCurveTablesPointer = OnlineHotfixManagerCVars::bDeferBroadcastCurveTableModified ? &ChangedCurveTables : nullptr;
|
|
|
|
for (FConfigSection::TConstIterator It(*AssetHotfixConfigSection); It; ++It)
|
|
{
|
|
FMoviePlayerProxy::BlockingTick();
|
|
++TotalPatchableAssets;
|
|
|
|
// Make sure the entry has a valid class name that we support
|
|
UClass* AssetClass = nullptr;
|
|
FString PatchableAssetClassesStr;
|
|
for (UClass* PatchableAssetClass : PatchableAssetClasses)
|
|
{
|
|
if (PatchableAssetClass)
|
|
{
|
|
PatchableAssetClassesStr += PatchableAssetClass->GetFName().ToString() + TEXT(" ");
|
|
|
|
if (PatchableAssetClass->GetFName().IsEqual(It.Key()))
|
|
{
|
|
AssetClass = PatchableAssetClass;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (AssetClass != nullptr)
|
|
{
|
|
TArray<FString> ProblemStrings;
|
|
|
|
FString DataLine(*It.Value().GetValue());
|
|
|
|
if (!DataLine.IsEmpty())
|
|
{
|
|
TArray<FString> Tokens;
|
|
DataLine.ParseIntoArray(Tokens, TEXT(";"));
|
|
if (Tokens.Num() == 3 || Tokens.Num() == 5)
|
|
{
|
|
const FString& AssetPath(Tokens[0]);
|
|
const FString& HotfixType(Tokens[1]);
|
|
|
|
if (!ShouldHotfixAsset(AssetPath))
|
|
{
|
|
//Child class says not to hotfix this asset so it shouldn't be included in the total
|
|
--TotalPatchableAssets;
|
|
continue;
|
|
}
|
|
|
|
bool bAddAssetToHotfixedList = false;
|
|
|
|
// Find or load the asset
|
|
UObject* Asset = FPackageName::IsValidLongPackageName(AssetPath, true) ? StaticLoadObject(AssetClass, nullptr, *AssetPath) : nullptr;
|
|
if (Asset != nullptr)
|
|
{
|
|
const FString RowUpdate(TEXT("RowUpdate"));
|
|
const FString AddRow(TEXT("AddRow"));
|
|
const FString TableUpdate(TEXT("TableUpdate"));
|
|
const FString CurveUpdate(TEXT("CurveUpdate"));
|
|
|
|
if (HotfixType == RowUpdate)
|
|
{
|
|
if (Tokens.Num() == 5)
|
|
{
|
|
// The hotfix line should be
|
|
// +DataTable=<data table path>;RowUpdate;<row name>;<column name>;<new value>
|
|
// +CurveTable=<curve table path>;RowUpdate;<row name>;<column name>;<new value>
|
|
// +CurveFloat=<curve float path>;RowUpdate;None;<column name>;<new value>
|
|
HotfixRowUpdate(Asset, AssetPath, Tokens[2], Tokens[3], Tokens[4], ProblemStrings, &ChangedDataTables, ChangedCurveTablesPointer);
|
|
bAddAssetToHotfixedList = ProblemStrings.Num() == 0;
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(FString::Printf(TEXT("Expected a hotfix type RowUpdate with 5 tokens but got %d"), Tokens.Num()));
|
|
}
|
|
}
|
|
else if (HotfixType == AddRow)
|
|
{
|
|
if (Tokens.Num() == 3)
|
|
{
|
|
// The hotfix line should be
|
|
// +DataTable=<data table path>;AddRow;"<json data>"
|
|
|
|
// We have to read json data as quoted string because tokenizing it creates extra unwanted characters.
|
|
FString JsonData;
|
|
|
|
// Json should be read in its entirety, if the whole buffer wasn't read the string is malformed.
|
|
int32 ReadLen = 0;
|
|
int32 InputLen = Tokens[2].Len();
|
|
if (FParse::QuotedString(*Tokens[2], JsonData, &ReadLen) && ReadLen == InputLen)
|
|
{
|
|
HotfixAddRow(Asset, AssetPath, JsonData, ProblemStrings);
|
|
bAddAssetToHotfixedList = ProblemStrings.Num() == 0;
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(TEXT("Json data wasn't able to be parsed as a quoted string. Check that we have opening and closing quotes around the json data."));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(FString::Printf(TEXT("Expected a hotfix type AddRow with 3 tokens but got %d"), Tokens.Num()));
|
|
}
|
|
}
|
|
else if (HotfixType == TableUpdate || HotfixType == CurveUpdate)
|
|
{
|
|
if (Tokens.Num() == 3)
|
|
{
|
|
// The hotfix line should be
|
|
// +DataTable=<data table path>;TableUpdate;"<json data>"
|
|
// +CurveTable=<curve table path>;TableUpdate;"<json data>"
|
|
// +CurveFloat=<curve float path>;CurveUpdate;"<json data>"
|
|
// +CurveVector=<curve vector path>;CurveUpdate;"<json data>"
|
|
// +CurveLinearColor=<curve linear color path>;CurveUpdate;"<json data>"
|
|
|
|
// We have to read json data as quoted string because tokenizing it creates extra unwanted characters.
|
|
FString JsonData;
|
|
|
|
// Json should be read in its entirety, if the whole buffer wasn't read the string is malformed.
|
|
int32 ReadLen = 0;
|
|
int32 InputLen = Tokens[2].Len();
|
|
if (FParse::QuotedString(*Tokens[2], JsonData, &ReadLen) && ReadLen == InputLen)
|
|
{
|
|
HotfixTableUpdate(Asset, AssetPath, JsonData, ProblemStrings);
|
|
bAddAssetToHotfixedList = ProblemStrings.Num() == 0;
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(TEXT("Json data wasn't able to be parsed as a quoted string. Check that we have opening and closing quotes around the json data."));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(FString::Printf(TEXT("Expected a hotfix type %s with 3 tokens but got %d"), *HotfixType, Tokens.Num()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(FString::Printf(TEXT("Unknown hotfix type %s"), *HotfixType));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ShouldWarnAboutMissingWhenPatchingFromIni(AssetPath))
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("Couldn't find or load asset '%s' (class '%s'). This asset will not be patched. Double check that your asset type and path string is correct."), *AssetPath, *AssetClass->GetPathName()));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
|
|
if (!bAddAssetToHotfixedList)
|
|
{
|
|
for (const FString& ProblemString : ProblemStrings)
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("[Item: %d] %s: %s"), TotalPatchableAssets, *GetPathNameSafe(Asset), *ProblemString);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We'll keep a reference to the successfully patched asset. We want to make sure our changes survive throughout
|
|
// this session, so we reference it to prevent it from being evicted from memory. It's OK if we end up re-patching
|
|
// the same asset multiple times per session.
|
|
AssetsHotfixedFromIniFiles.Add(Asset);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("[Item: %d] Wasn't able to parse the data with semicolon separated values. Expecting 3 or 5 arguments but parsed %d."), TotalPatchableAssets, Tokens.Num());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("[Item: %d] Empty value given for '%s' entry!"), TotalPatchableAssets, *It.Key().ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Error, TEXT("[Item: %d] Invalid patchable asset type '%s' - supported types: %s"), TotalPatchableAssets, *It.Key().ToString(), *PatchableAssetClassesStr);
|
|
}
|
|
}
|
|
|
|
for (UDataTable* DataTable : ChangedDataTables)
|
|
{
|
|
if (DataTable != nullptr)
|
|
{
|
|
DataTable->HandleDataTableChanged();
|
|
}
|
|
}
|
|
|
|
for (UCurveTable* CurveTable : ChangedCurveTables)
|
|
{
|
|
if (CurveTable != nullptr)
|
|
{
|
|
CurveTable->OnCurveTableChanged().Broadcast();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TotalPatchableAssets == 0)
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("No assets were found in the 'AssetHotfix' section in the Game .ini file. No patching needed."));
|
|
}
|
|
else if (TotalPatchableAssets == AssetsHotfixedFromIniFiles.Num())
|
|
{
|
|
UE_LOG(LogHotfixManager, Display, TEXT("Successfully patched all %i assets from the 'AssetHotfix' section in the Game .ini file. These assets will be forced to remain loaded."), AssetsHotfixedFromIniFiles.Num());
|
|
}
|
|
else
|
|
{
|
|
// Some assets will fail to patch due to their plugins not being installed or mounted when PatchAssetsFromIniFiles is called.
|
|
// This behavior is a limitation of having a hotfix for the overall game and will require plugin specific hotfixes to address.
|
|
// PatchAssetsFromIniFiles must be called again when plugins change to allow patching assets when their plugin is mounted.
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Only %i of %i assets were successfully patched from 'AssetHotfix' section in the Game .ini file. The patched assets will be forced to remain loaded. Any assets that failed to patch may be left in an invalid state! Failure may occur due to the plugin not being installed and mounted."), AssetsHotfixedFromIniFiles.Num(), TotalPatchableAssets);
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::ReloadConfigsFromIniFiles()
|
|
{
|
|
if (HotfixFileList.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FString> ClassesToReload;
|
|
|
|
for (const FCloudFileHeader& FileHeader : HotfixFileList)
|
|
{
|
|
if (FileHeader.FileName.EndsWith(TEXT(".INI")))
|
|
{
|
|
const FString ProcessedName = BuildConfigCacheKey(GetStrippedConfigFileName(FileHeader.FileName));
|
|
for (int32 Index = 0; Index < IniBackups.Num(); Index++)
|
|
{
|
|
const FConfigFileBackup& BackupFile = IniBackups[Index];
|
|
if (IniBackups[Index].IniName == ProcessedName)
|
|
{
|
|
ClassesToReload.Append(BackupFile.ClassesReloaded);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 NumObjectsReloaded = 0;
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
if (ClassesToReload.Num() > 0)
|
|
{
|
|
FlushAsyncLoading();
|
|
|
|
TArray<UClass*> RestoredClasses;
|
|
RestoredClasses.Reserve(ClassesToReload.Num());
|
|
for (int32 Index = 0; Index < ClassesToReload.Num(); Index++)
|
|
{
|
|
UClass* Class = FindObject<UClass>(nullptr, *ClassesToReload[Index], true);
|
|
if (Class != nullptr)
|
|
{
|
|
// Add this to the list to check against
|
|
RestoredClasses.Add(Class);
|
|
}
|
|
}
|
|
|
|
for (UClass* Class : RestoredClasses)
|
|
{
|
|
if (Class->HasAnyClassFlags(CLASS_Config))
|
|
{
|
|
TArray<UObject*> Objects;
|
|
GetObjectsOfClass(Class, Objects, true, RF_NoFlags);
|
|
for (UObject* Object : Objects)
|
|
{
|
|
if (IsValid(Object))
|
|
{
|
|
UE_LOG(LogHotfixManager, Verbose, TEXT("Reloading %s"), *Object->GetPathName());
|
|
Object->ReloadConfig();
|
|
NumObjectsReloaded++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Reloading config for %d changed classes took %f seconds reloading %d objects"),
|
|
ClassesToReload.Num(), FPlatformTime::Seconds() - StartTime, NumObjectsReloaded);
|
|
}
|
|
|
|
void UOnlineHotfixManager::HotfixRowUpdate(
|
|
UObject* Asset,
|
|
const FString& AssetPath,
|
|
const FString& RowName,
|
|
const FString& ColumnName,
|
|
const FString& NewValue,
|
|
TArray<FString>& ProblemStrings,
|
|
TSet<UDataTable*>* ChangedDataTables,
|
|
TSet<UCurveTable*>* ChangedCurveTables)
|
|
{
|
|
if (AssetPath.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The table's path is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
if (RowName.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The row name is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
if (ColumnName.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The column name is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
if (NewValue.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The new value is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
|
|
UDataTable* DataTable = Cast<UDataTable>(Asset);
|
|
UCurveTable* CurveTable = Cast<UCurveTable>(Asset);
|
|
UCurveFloat* CurveFloat = Cast<UCurveFloat>(Asset);
|
|
if (DataTable != nullptr)
|
|
{
|
|
// Edit the row with the new value.
|
|
bool bWasDataTableChanged = false;
|
|
|
|
FProperty* DataTableRowProperty = PropertyAccessUtil::FindPropertyByName(FName(*ColumnName), DataTable->GetRowStruct());
|
|
if (DataTableRowProperty)
|
|
{
|
|
// See what type of property this is.
|
|
FNumericProperty* NumProp = CastField<FNumericProperty>(DataTableRowProperty);
|
|
FStrProperty* StrProp = CastField<FStrProperty>(DataTableRowProperty);
|
|
FNameProperty* NameProp = CastField<FNameProperty>(DataTableRowProperty);
|
|
FSoftObjectProperty* SoftObjProp = CastField<FSoftObjectProperty>(DataTableRowProperty);
|
|
|
|
// Get the row data by name.
|
|
static const FString Context = FString(TEXT("UOnlineHotfixManager::PatchAssetsFromIniFiles"));
|
|
uint8* DataTableRow = DataTable->FindRowUnchecked(FName(*RowName));
|
|
if (DataTableRow)
|
|
{
|
|
uint8* RowData = DataTableRowProperty->ContainerPtrToValuePtr<uint8>(DataTableRow, 0);
|
|
if (RowData)
|
|
{
|
|
// Numeric property
|
|
if (NumProp)
|
|
{
|
|
if (NewValue.IsNumeric())
|
|
{
|
|
// Integer
|
|
if (NumProp->IsInteger())
|
|
{
|
|
const int64 OldPropertyValue = NumProp->GetSignedIntPropertyValue(RowData);
|
|
const int64 NewPropertyValue = FCString::Atoi(*NewValue);
|
|
NumProp->SetIntPropertyValue(RowData, NewPropertyValue);
|
|
OnHotfixTableValueInt64(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
bWasDataTableChanged = true;
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Data table %s row %s updated column %s from %" INT64_FMT " to %" INT64_FMT "."), *AssetPath, *RowName, *ColumnName, OldPropertyValue, NewPropertyValue);
|
|
}
|
|
// Float
|
|
else
|
|
{
|
|
const double OldPropertyValue = NumProp->GetFloatingPointPropertyValue(RowData);
|
|
const double NewPropertyValue = FCString::Atod(*NewValue);
|
|
NumProp->SetFloatingPointPropertyValue(RowData, NewPropertyValue);
|
|
OnHotfixTableValueDouble(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
bWasDataTableChanged = true;
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Data table %s row %s updated column %s from %.2f to %.2f."), *AssetPath, *RowName, *ColumnName, OldPropertyValue, NewPropertyValue);
|
|
}
|
|
}
|
|
// Not a number.
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The new value %s is not a number when it should be."), *NewValue));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
// String property
|
|
else if (StrProp)
|
|
{
|
|
const FString OldPropertyValue = StrProp->GetPropertyValue(RowData);
|
|
const FString NewPropertyValue = NewValue;
|
|
StrProp->SetPropertyValue(RowData, NewPropertyValue);
|
|
OnHotfixTableValueString(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
bWasDataTableChanged = true;
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue, *NewPropertyValue);
|
|
}
|
|
// FName property
|
|
else if (NameProp)
|
|
{
|
|
const FName OldPropertyValue = NameProp->GetPropertyValue(RowData);
|
|
const FName NewPropertyValue = FName(*NewValue);
|
|
NameProp->SetPropertyValue(RowData, NewPropertyValue);
|
|
OnHotfixTableValueName(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
bWasDataTableChanged = true;
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue.ToString(), *NewPropertyValue.ToString());
|
|
}
|
|
// Soft Object property
|
|
else if (SoftObjProp)
|
|
{
|
|
FSoftObjectPtr OldPropertyValue = SoftObjProp->GetPropertyValue(RowData);
|
|
FSoftObjectPtr NewPropertyValue(NewValue);
|
|
SoftObjProp->SetPropertyValue(RowData, NewPropertyValue);
|
|
OnHotfixTableValueSoftObject(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
bWasDataTableChanged = true;
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue.ToString(), *NewPropertyValue.ToString());
|
|
}
|
|
else
|
|
{
|
|
// Evaluate supported property types, i.e. FBoolProperty, and attempt to assign the value
|
|
const FString OldPropertyValue = DataTableUtils::GetPropertyValueAsString(DataTableRowProperty, (uint8*)DataTableRow, EDataTableExportFlags::UseSimpleText);
|
|
FString Error = DataTableUtils::AssignStringToProperty(NewValue, DataTableRowProperty, (uint8*)DataTableRow);
|
|
|
|
if (Error.IsEmpty())
|
|
{
|
|
bWasDataTableChanged = true;
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue, *NewValue);
|
|
}
|
|
else
|
|
{
|
|
FString Problem(FString::Printf(TEXT("Failed to update data table %s row %s column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue, *NewValue));
|
|
ProblemStrings.Add(MoveTemp(Problem));
|
|
ProblemStrings.Add(MoveTemp(Error));
|
|
}
|
|
}
|
|
}
|
|
// Row data wasn't found.
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The data table row data for row %s was not found."), *RowName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
// Row wasn't found.
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The data table row %s was not found."), *RowName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
// Property wasn't found.
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("Couldn't find the data table property named %s. Check the spelling."), *ColumnName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
|
|
if (bWasDataTableChanged)
|
|
{
|
|
if (ChangedDataTables == nullptr)
|
|
{
|
|
DataTable->HandleDataTableChanged();
|
|
}
|
|
else
|
|
{
|
|
ChangedDataTables->Add(DataTable);
|
|
}
|
|
}
|
|
}
|
|
else if (CurveTable)
|
|
{
|
|
bool bWasCurveTableChanged = false;
|
|
|
|
if (ColumnName.IsNumeric())
|
|
{
|
|
// Get the row data by name.
|
|
static const FString Context = FString(TEXT("UOnlineHotfixManager::PatchAssetsFromIniFiles"));
|
|
FRealCurve* CurveTableRow = CurveTable->FindCurve(FName(*RowName), Context);
|
|
|
|
if (CurveTableRow)
|
|
{
|
|
// Edit the row with the new value.
|
|
const float KeyTime = FCString::Atof(*ColumnName);
|
|
FKeyHandle Key = CurveTableRow->FindKey(KeyTime);
|
|
|
|
bool bWasExistingKey = CurveTableRow->IsKeyHandleValid(Key);
|
|
|
|
if (NewValue.IsNumeric())
|
|
{
|
|
const float OldPropertyValue = CurveTableRow->GetKeyValue(Key);
|
|
const float NewPropertyValue = FCString::Atof(*NewValue);
|
|
Key = CurveTableRow->UpdateOrAddKey(KeyTime, NewPropertyValue);
|
|
if (CurveTableRow->IsKeyHandleValid(Key))
|
|
{
|
|
OnHotfixTableValueFloat(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
bWasCurveTableChanged = true;
|
|
|
|
if (bWasExistingKey)
|
|
{
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Curve table %s row %s updated column %s from %.2f to %.2f."), *AssetPath, *RowName, *ColumnName, OldPropertyValue, NewPropertyValue);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Curve table %s row %s added column %s with value %.2f."), *AssetPath, *RowName, *ColumnName, NewPropertyValue);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("Unable to update Curve table %s row %s column %s with value %.2f."), *AssetPath, *RowName, *ColumnName, NewPropertyValue));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The new value %s at key %f is not a number when it should be."), *NewValue, KeyTime));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The curve table row for row name %s was not found."), *RowName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The column name %s is not a number when it should be."), *ColumnName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
|
|
if (bWasCurveTableChanged)
|
|
{
|
|
if (ChangedCurveTables == nullptr)
|
|
{
|
|
CurveTable->OnCurveTableChanged().Broadcast();
|
|
}
|
|
else
|
|
{
|
|
ChangedCurveTables->Add(CurveTable);
|
|
}
|
|
}
|
|
}
|
|
else if (CurveFloat)
|
|
{
|
|
if (ColumnName.IsNumeric())
|
|
{
|
|
// Edit the curve with the new value.
|
|
const float KeyTime = FCString::Atof(*ColumnName);
|
|
FKeyHandle Key = CurveFloat->FloatCurve.FindKey(KeyTime);
|
|
if (CurveFloat->FloatCurve.IsKeyHandleValid(Key))
|
|
{
|
|
if (NewValue.IsNumeric())
|
|
{
|
|
const float OldPropertyValue = CurveFloat->FloatCurve.GetKeyValue(Key);
|
|
const float NewPropertyValue = FCString::Atof(*NewValue);
|
|
CurveFloat->FloatCurve.SetKeyValue(Key, NewPropertyValue);
|
|
OnHotfixTableValueFloat(*Asset, RowName, ColumnName, OldPropertyValue, NewPropertyValue);
|
|
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("Curve float %s updated column %s from %.2f to %.2f."), *AssetPath, *ColumnName, OldPropertyValue, NewPropertyValue);
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The new value %s is not a number when it should be."), *NewValue));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The column name %s isn't a valid key into the curve float."), *ColumnName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FString Problem(FString::Printf(TEXT("The column name %s is not a number when it should be."), *ColumnName));
|
|
ProblemStrings.Add(Problem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(TEXT("The Asset isn't a Data Table, Curve Table, or Curve Float."));
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::HotfixAddRow(
|
|
UObject* Asset,
|
|
const FString& AssetPath,
|
|
const FString& JsonData,
|
|
TArray<FString>& ProblemStrings,
|
|
TSet<UDataTable*>* ChangedDataTables)
|
|
{
|
|
if (AssetPath.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The table's path is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
if (JsonData.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The JSON data is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
|
|
if (UDataTable* DataTable = Cast<UDataTable>(Asset))
|
|
{
|
|
FName RowName = NAME_None;
|
|
// Allow hotfix to be applied multiple times
|
|
const bool bRemoveDuplicate = true;
|
|
if (!DataTableUtils::AddRowJSON(*DataTable, JsonData, ProblemStrings, bRemoveDuplicate, &RowName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnHotfixTableAddRow(*DataTable, RowName);
|
|
|
|
if (ChangedDataTables == nullptr)
|
|
{
|
|
DataTable->HandleDataTableChanged();
|
|
}
|
|
else
|
|
{
|
|
ChangedDataTables->Add(DataTable);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::HotfixTableUpdate(UObject* Asset, const FString& AssetPath, const FString& JsonData, TArray<FString>& ProblemStrings)
|
|
{
|
|
if (AssetPath.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The table's path is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
if (JsonData.IsEmpty())
|
|
{
|
|
ProblemStrings.Add(TEXT("The JSON data is empty. We cannot continue the hotfix."));
|
|
return;
|
|
}
|
|
|
|
// Let's import over the object in place.
|
|
UCurveTable* CurveTable = Cast<UCurveTable>(Asset);
|
|
UDataTable* DataTable = Cast<UDataTable>(Asset);
|
|
UCurveFloat* CurveFloat = Cast<UCurveFloat>(Asset);
|
|
UCurveVector* CurveVector = Cast<UCurveVector>(Asset);
|
|
UCurveLinearColor* CurveLinearColor = Cast<UCurveLinearColor>(Asset);
|
|
if (CurveTable != nullptr)
|
|
{
|
|
ProblemStrings.Append(CurveTable->CreateTableFromJSONString(JsonData));
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Curve table %s updated."), *AssetPath);
|
|
}
|
|
else if (DataTable != nullptr)
|
|
{
|
|
ProblemStrings.Append(DataTable->CreateTableFromJSONString(JsonData));
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Data table %s updated."), *AssetPath);
|
|
}
|
|
else if (CurveFloat != nullptr)
|
|
{
|
|
CurveFloat->ImportFromJSONString(JsonData, ProblemStrings);
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Curve float %s updated."), *AssetPath);
|
|
}
|
|
else if (CurveVector != nullptr)
|
|
{
|
|
CurveVector->ImportFromJSONString(JsonData, ProblemStrings);
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Curve vector %s updated."), *AssetPath);
|
|
}
|
|
else if (CurveLinearColor != nullptr)
|
|
{
|
|
CurveLinearColor->ImportFromJSONString(JsonData, ProblemStrings);
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Curve linear color %s updated."), *AssetPath);
|
|
}
|
|
else
|
|
{
|
|
ProblemStrings.Add(TEXT("Unable to hotfix this asset type. Only DataTables, CurveTables and Curve data types are supported."));
|
|
}
|
|
}
|
|
|
|
bool UOnlineHotfixManager::ShouldPerformHotfix()
|
|
{
|
|
return IsRunningGame() || IsRunningDedicatedServer() || IsRunningClientOnly();
|
|
}
|
|
|
|
FString UOnlineHotfixManager::GetDedicatedServerPrefix() const
|
|
{
|
|
return TEXT("DedicatedServer");
|
|
}
|
|
|
|
bool UOnlineHotfixManager::ShouldHotfixAsset(const FString& AssetPath) const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
UWorld* UOnlineHotfixManager::GetWorld() const
|
|
{
|
|
return OwnerWorld.IsValid() ? OwnerWorld.Get() : nullptr;
|
|
}
|
|
|
|
void UOnlineHotfixManager::StopTrackingInvalidHotfixedAssets()
|
|
{
|
|
AssetsHotfixedFromIniFiles.RemoveAllSwap([](const UObject* Obj) { return !IsValid(Obj); });
|
|
}
|
|
|
|
void UOnlineHotfixManager::HotfixDynamicBranch(const FName& Tag, const FName& Branch, class FConfigModificationTracker* ModificationTracker)
|
|
{
|
|
const FString HotfixBaseName = Tag.ToString() + Branch.ToString();
|
|
const TArray<TPair<FString, FString>>* HotfixesForTag = DynamicHotfixContents.Find(*HotfixBaseName);
|
|
|
|
UE_LOG(LogHotfixManager, VeryVerbose, TEXT("[DynamicHotfixContents] HotfixDynamicBranch: Searching for hotfix for base name: %s. Result: %s"), *HotfixBaseName, HotfixesForTag ? TEXT("found") : TEXT("not found"));
|
|
|
|
if (HotfixesForTag)
|
|
{
|
|
for (const TPair<FString, FString>& HotfixFilenameAndContents : *HotfixesForTag)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("[DynamicHotfixContents] HotfixDynamicBranch: applying hotfix %s for tag %s on branch %s"), *HotfixFilenameAndContents.Key, *Tag.ToString(), *Branch.ToString());
|
|
|
|
FConfigBranch* BranchToHotfix = GConfig->FindBranch(Branch, FString());
|
|
if (BranchToHotfix && !BranchToHotfix->AddDynamicLayerStringToHierarchy(HotfixFilenameAndContents.Key, HotfixFilenameAndContents.Value, Tag, DynamicLayerPriority::Hotfix, ModificationTracker))
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("[DynamicHotfixContents] HotfixDynamicBranch: failed to apply hotfix"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UOnlineHotfixManager::ReloadObjectsAffectedByConfigFile(const FString& IniDataFileName, const FString& IniData, const FString& ConfigFilename, TArray<FString>& ReloadedClassesPathNames, bool bUseLoadConfig)
|
|
{
|
|
TArray<UClass*> Classes;
|
|
TArray<UObject*> PerObjectConfigObjects;
|
|
int32 StartIndex = 0;
|
|
int32 EndIndex = 0;
|
|
TSet<FString> UpdatedSectionNames;
|
|
// Find the set of object classes that were affected
|
|
while (StartIndex >= 0 && StartIndex < IniData.Len() && EndIndex >= StartIndex)
|
|
{
|
|
// Find the next section header
|
|
StartIndex = IniData.Find(TEXT("["), ESearchCase::IgnoreCase, ESearchDir::FromStart, StartIndex);
|
|
if (StartIndex > -1)
|
|
{
|
|
// Find the ending section identifier
|
|
EndIndex = IniData.Find(TEXT("]"), ESearchCase::IgnoreCase, ESearchDir::FromStart, StartIndex);
|
|
if (EndIndex > StartIndex)
|
|
{
|
|
// Ignore square brackets in the middle of string
|
|
// - per object section starts with new line
|
|
// - there's no " character between opening bracket and line start
|
|
const bool bStartsWithNewLine = (StartIndex == 0) || (IniData[StartIndex - 1] == TEXT('\n'));
|
|
if (!bStartsWithNewLine)
|
|
{
|
|
bool bStartsInsideString = false;
|
|
for (int32 CharIdx = StartIndex - 1; CharIdx >= 0; CharIdx--)
|
|
{
|
|
const bool bHasStringMarker = (IniData[CharIdx] == TEXT('"'));
|
|
if (bHasStringMarker)
|
|
{
|
|
bStartsInsideString = true;
|
|
break;
|
|
}
|
|
|
|
const bool bHasNewLineMarker = (IniData[CharIdx] == TEXT('\n'));
|
|
if (bHasNewLineMarker)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bStartsInsideString)
|
|
{
|
|
StartIndex = EndIndex;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
UpdatedSectionNames.Emplace(IniData.Mid(StartIndex + 1, EndIndex - StartIndex - 1));
|
|
|
|
int32 PerObjectNameIndex = IniData.Find(TEXT(" "), ESearchCase::IgnoreCase, ESearchDir::FromStart, StartIndex);
|
|
|
|
const TCHAR* AssetHotfixIniHACK = TEXT("[AssetHotfix]");
|
|
if (FCString::Strnicmp(*IniData + StartIndex, AssetHotfixIniHACK, FCString::Strlen(AssetHotfixIniHACK)) == 0)
|
|
{
|
|
// HACK - Make AssetHotfix the last element in the ini file so that this parsing isn't affected by it for now
|
|
break;
|
|
}
|
|
|
|
// Per object config entries will have a space in the name, but classes won't
|
|
if (PerObjectNameIndex == -1 || PerObjectNameIndex > EndIndex)
|
|
{
|
|
const TCHAR* ScriptHeader = TEXT("[/Script/");
|
|
const TCHAR* GameHeader = TEXT("[/Game/");
|
|
if (FCString::Strnicmp(*IniData + StartIndex, ScriptHeader, FCString::Strlen(ScriptHeader)) == 0)
|
|
{
|
|
const int32 ScriptSectionTag = 9;
|
|
// Snip the text out and try to find the class for that
|
|
const FString PackageClassName = IniData.Mid(StartIndex + ScriptSectionTag, EndIndex - StartIndex - ScriptSectionTag);
|
|
// Find the class for this so we know what to update
|
|
UClass* Class = FindObject<UClass>(nullptr, *PackageClassName, true);
|
|
if (Class)
|
|
{
|
|
// Add this to the list to check against
|
|
Classes.Add(Class);
|
|
ReloadedClassesPathNames.AddUnique(Class->GetPathName());
|
|
}
|
|
}
|
|
else if (FCString::Strnicmp(*IniData + StartIndex, GameHeader, FCString::Strlen(GameHeader)) == 0)
|
|
{
|
|
const int32 GameSectionTag = 1;
|
|
// Snip the text out and try to find the class for that
|
|
const FString PackageClassName = IniData.Mid(StartIndex + GameSectionTag, EndIndex - StartIndex - GameSectionTag);
|
|
UBlueprintGeneratedClass* BPGeneratedClass = LoadObject<UBlueprintGeneratedClass>(nullptr, *PackageClassName);
|
|
if (BPGeneratedClass)
|
|
{
|
|
// Add this to the list to check against
|
|
Classes.Add(BPGeneratedClass);
|
|
ReloadedClassesPathNames.AddUnique(BPGeneratedClass->GetPathName());
|
|
}
|
|
}
|
|
}
|
|
// Handle the per object config case by finding the object for reload
|
|
else
|
|
{
|
|
const int32 ClassNameStart = PerObjectNameIndex + 1;
|
|
const FString ClassName = IniData.Mid(ClassNameStart, EndIndex - ClassNameStart);
|
|
|
|
// Look up the class to search for
|
|
UClass* ObjectClass = UClass::TryFindTypeSlow<UClass>(ClassName);
|
|
|
|
if (ObjectClass)
|
|
{
|
|
const int32 Count = PerObjectNameIndex - StartIndex - 1;
|
|
const FString PerObjectName = IniData.Mid(StartIndex + 1, Count);
|
|
|
|
// Explicitly search the transient package (won't update non-transient objects)
|
|
UObject* PerObject = StaticFindFirstObject(ObjectClass, *PerObjectName, EFindFirstObjectOptions::NativeFirst);
|
|
if (PerObject != nullptr)
|
|
{
|
|
PerObjectConfigObjects.Add(PerObject);
|
|
ReloadedClassesPathNames.AddUnique(ObjectClass->GetPathName());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHotfixManager, Warning, TEXT("Specified per-object class %s was not found"), *ClassName);
|
|
}
|
|
}
|
|
StartIndex = EndIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 NumObjectsReloaded = 0;
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
// Now that we have a list of classes to update, we can iterate objects and reload
|
|
for (UClass* Class : Classes)
|
|
{
|
|
if (Class->HasAnyClassFlags(CLASS_Config))
|
|
{
|
|
TArray<UObject*> Objects;
|
|
GetObjectsOfClass(Class, Objects, true, RF_NoFlags);
|
|
for (UObject* Object : Objects)
|
|
{
|
|
if (IsValid(Object))
|
|
{
|
|
// Force a reload of the config vars
|
|
UE_LOG(LogHotfixManager, Verbose, TEXT("Reloading %s"), *Object->GetPathName());
|
|
bUseLoadConfig ? Object->LoadConfig() : Object->ReloadConfig();
|
|
NumObjectsReloaded++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reload any PerObjectConfig objects that were affected
|
|
for (UObject* ReloadObject : PerObjectConfigObjects)
|
|
{
|
|
UE_LOG(LogHotfixManager, Verbose, TEXT("Reloading %s"), *ReloadObject->GetPathName());
|
|
bUseLoadConfig ? ReloadObject->LoadConfig() : ReloadObject->ReloadConfig();
|
|
NumObjectsReloaded++;
|
|
}
|
|
|
|
FCoreDelegates::TSOnConfigSectionsChanged().Broadcast(ConfigFilename, UpdatedSectionNames);
|
|
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Updating config from %s took %f seconds and reloaded %d objects"),
|
|
*IniDataFileName, FPlatformTime::Seconds() - StartTime, NumObjectsReloaded);
|
|
}
|
|
|
|
#if UE_ALLOW_EXEC_COMMANDS
|
|
struct FHotfixManagerExec :
|
|
public FSelfRegisteringExec
|
|
{
|
|
protected:
|
|
virtual bool Exec_Runtime(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override
|
|
{
|
|
if (FParse::Command(&Cmd, TEXT("HOTFIX")))
|
|
{
|
|
UOnlineHotfixManager* HotfixManager = UOnlineHotfixManager::Get(InWorld);
|
|
if (HotfixManager != nullptr)
|
|
{
|
|
HotfixManager->StartHotfixProcess();
|
|
}
|
|
return true;
|
|
}
|
|
else if (FParse::Command(&Cmd, TEXT("TESTHOTFIXSORT")))
|
|
{
|
|
TArray<FCloudFileHeader> TestList;
|
|
FCloudFileHeader Header;
|
|
Header.FileName = TEXT("SomeRandom.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("DedicatedServerGame.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("pakchunk1-PS4_P.pak");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("EN_Game.locres");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("DefaultGame.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("Ver-1234_DefaultEngine.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("PS4_DefaultEngine.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("DefaultEngine.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("pakchunk0-PS4_P.pak");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("PS4_DefaultGame.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("Ver-1234_PS4_DefaultGame.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("PS4_Ver-1234_DefaultGame.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("AnotherRandom.ini");
|
|
TestList.Add(Header);
|
|
Header.FileName = TEXT("DedicatedServerEngine.ini");
|
|
TestList.Add(Header);
|
|
TestList.Sort<FHotfixFileSortPredicate>(FHotfixFileSortPredicate(TEXT("PS4_"), TEXT("DedicatedServer"), TEXT("Default")));
|
|
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfixing sort is:"));
|
|
for (const FCloudFileHeader& FileHeader : TestList)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("\t%s"), *FileHeader.FileName);
|
|
}
|
|
|
|
TArray<FString> TestList2;
|
|
TestList2.Add(TEXT("SomeRandom.ini"));
|
|
TestList2.Add(TEXT("DefaultGame.ini"));
|
|
TestList2.Add(TEXT("PS4_DefaultEngine.ini"));
|
|
TestList2.Add(TEXT("DedicatedServerEngine.ini"));
|
|
TestList2.Add(TEXT("DedicatedServerGame.ini"));
|
|
TestList2.Add(TEXT("DefaultEngine.ini"));
|
|
TestList2.Add(TEXT("PS4_DefaultGame.ini"));
|
|
TestList2.Add(TEXT("AnotherRandom.ini"));
|
|
TestList2.Sort<FHotfixFileSortPredicate>(FHotfixFileSortPredicate(TEXT("PS4_"), TEXT("DedicatedServer"), TEXT("Default")));
|
|
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Hotfixing PAK INI file sort is:"));
|
|
for (const FString& IniName : TestList2)
|
|
{
|
|
UE_LOG(LogHotfixManager, Log, TEXT("\t%s"), *IniName);
|
|
}
|
|
return true;
|
|
}
|
|
else if (FParse::Command(&Cmd, TEXT("LOGHOTFIXASSETLIST")))
|
|
{
|
|
if (UOnlineHotfixManager* HotfixManager = UOnlineHotfixManager::Get(InWorld))
|
|
{
|
|
int32 CurrentAsset = 0;
|
|
const int32 AssetCount = HotfixManager->AssetsHotfixedFromIniFiles.Num();
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Logging contetents of AssetsHotfixedFromIniFiles. Asset count: %d"), AssetCount);
|
|
for (const TObjectPtr<UObject>& ObjPtr : HotfixManager->AssetsHotfixedFromIniFiles)
|
|
{
|
|
++CurrentAsset;
|
|
UE_LOG(LogHotfixManager, Log, TEXT("\t[%d/%d]: %s"), CurrentAsset, AssetCount, *GetPathNameSafe(ObjPtr.Get()));
|
|
}
|
|
UE_LOG(LogHotfixManager, Log, TEXT("Logging complete."));
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
static FHotfixManagerExec HotfixManagerExec;
|
|
#endif // UE_ALLOW_EXEC_COMMANDS
|