// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Misc/ConfigCacheIni.h" #include "Interfaces/OnlineTitleFileInterface.h" #include "OnlineHotfixManager.generated.h" #define UE_API HOTFIX_API struct FCloudFileHeader; HOTFIX_API DECLARE_LOG_CATEGORY_EXTERN(LogHotfixManager, Display, All); class FAsyncLoadingFlushContext; UENUM() enum class EHotfixResult : uint8 { /** Failed to apply the hotfix */ Failed, /** Hotfix succeeded and is ready to go */ Success, /** Hotfix process succeeded but there were no changes applied */ SuccessNoChange, /** Hotfix succeeded and requires the current level to be reloaded to take effect */ SuccessNeedsReload, /** Hotfix succeeded and requires the process restarted to take effect */ SuccessNeedsRelaunch }; /** * Delegate fired when a check for hotfix files (but not application) completes * * @param EHotfixResult status on what happened */ DECLARE_DELEGATE_OneParam(FOnHotfixAvailableComplete, EHotfixResult); /** * Delegate fired when the hotfix process has completed * * @param EHotfixResult status on what happened */ DECLARE_MULTICAST_DELEGATE_OneParam(FOnHotfixComplete, EHotfixResult); typedef FOnHotfixComplete::FDelegate FOnHotfixCompleteDelegate; /** * Delegate fired as progress of hotfix file reading happens * * @param NumDownloaded the number of files downloaded so far * @param TotalFiles the total number of files part of the hotfix * @param NumBytes the number of bytes processed so far * @param TotalBytes the total size of the hotfix data */ DECLARE_MULTICAST_DELEGATE_FourParams(FOnHotfixProgress, uint32, uint32, uint64, uint64); typedef FOnHotfixProgress::FDelegate FOnHotfixProgressDelegate; /** * Delegate fired for each new/updated file after it is applied * * @param FriendlyName the human readable version of the file name (DefaultEngine.ini) * @param CachedFileName the full path to the file on disk */ DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHotfixProcessedFile, const FString&, const FString&); typedef FOnHotfixProcessedFile::FDelegate FOnHotfixProcessedFileDelegate; /** * Delegate fired for each removed file * * @param FriendlyName the human readable version of the file name (DefaultEngine.ini) */ DECLARE_MULTICAST_DELEGATE_OneParam(FOnHotfixRemovedFile, const FString&); typedef FOnHotfixRemovedFile::FDelegate FOnHotfixRemovedFileDelegate; /** * Delegate fired for each added/updated file * * @param FriendlyName the human readable version of the file name (DefaultEngine.ini) * @param FileContents the preprocessed contents of the file. */ DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHotfixUpdatedFile, const FString&, const TArray&); typedef FOnHotfixUpdatedFile::FDelegate FOnHotfixUpdatedFileDelegate; /** * This class manages the downloading and application of hotfix data * Hotfix data is a set of non-executable files downloaded and applied to the game. * The base implementation knows how to handle INI, PAK, and locres files. * NOTE: Each INI/PAK file must be prefixed by the platform name they are targeted at */ UCLASS(MinimalAPI, Config=Engine) class UOnlineHotfixManager : public UObject { GENERATED_BODY() protected: /** The online interface to use for downloading the hotfix files */ IOnlineTitleFilePtr OnlineTitleFile; /** Callbacks for when the title file interface is done */ FOnEnumerateFilesCompleteDelegate OnEnumerateFilesCompleteDelegate; FOnReadFileProgressDelegate OnReadFileProgressDelegate; FOnReadFileCompleteDelegate OnReadFileCompleteDelegate; FDelegateHandle OnEnumerateFilesCompleteDelegateHandle; FDelegateHandle OnEnumerateFilesForAvailabilityCompleteDelegateHandle; FDelegateHandle OnReadFileProgressDelegateHandle; FDelegateHandle OnReadFileCompleteDelegateHandle; /** * Delegate fired when the hotfix process has completed * * @param status of the hotfix process */ DEFINE_ONLINE_DELEGATE_ONE_PARAM(OnHotfixComplete, EHotfixResult); /** * Delegate fired as the hotfix files are read */ DEFINE_ONLINE_DELEGATE_FOUR_PARAM(OnHotfixProgress, uint32, uint32, uint64, uint64); /** * Delegate fired for each new/updated file after it is applied */ DEFINE_ONLINE_DELEGATE_TWO_PARAM(OnHotfixProcessedFile, const FString&, const FString&); /** * Delegate fired for each removed file */ DEFINE_ONLINE_DELEGATE_ONE_PARAM(OnHotfixRemovedFile, const FString&); /** * Delegate fired for each added/updated file */ DEFINE_ONLINE_DELEGATE_TWO_PARAM(OnHotfixUpdatedFile, const FString&, const TArray&); struct FPendingFileDLProgress { uint64 Progress; FPendingFileDLProgress() { Progress = 0; } }; struct FConfigFileBackup { /** Name of the ini file backed up*/ FString IniName; /** Previous ini data backed up */ FConfigFile ConfigData; /** UClasses reloaded as a result of the current ini */ TArray ClassesReloaded; }; /** Holds which files are pending download */ TMap PendingHotfixFiles; /** The filtered list of files that are part of the hotfix */ TArray HotfixFileList; /** The last set of hotfix files that was applied so we can determine whether we are up to date or not */ TArray LastHotfixFileList; /** The set of hotfix files that have changed from the last time we applied them */ TArray ChangedHotfixFileList; /** The set of hotfix files that have been removed from the last time we applied them */ TArray RemovedHotfixFileList; /** Holds which files have been mounted for unmounting */ TArray MountedPakFiles; /** Backup copies of INI files that change during hotfixing so they can be undone afterward */ TArray IniBackups; /** Used to match any PAK files for this platform */ FString PlatformPrefix; /** Used to match any server-only hotfixes */ FString ServerPrefix; /** Normally will be "Default" but could be different if we have a debug prefix */ FString DefaultPrefix; /** Holds a chunk of string that will be swapped for Game during processing pak files (MyGame/Content/Maps -> /Game/Maps) */ FString GameContentPath; /** Tracks how many files are being processed as part of the hotfix */ uint32 TotalFiles; uint32 NumDownloaded; /** Tracks the size of the files being processed as part of the hotfix */ uint64 TotalBytes; uint64 NumBytes; /** Some title file interfaces aren't re-entrant so handle it ourselves */ bool bHotfixingInProgress; /** Asynchronously flush async loading before starting the hotfixing process. */ TUniquePtr AsyncFlushContext; /** Set to true if any PAK file contains an update to a level that is currently loaded */ bool bHotfixNeedsMapReload; #if !UE_BUILD_SHIPPING /** Whether we want to log all of the files that are in a mounted pak file or not */ bool bLogMountedPakContents; #endif /** * If we have removed or changed a currently mounted PAK file, then we'll need to restart the app * because there's no simple undo for objects that were loaded and possibly rooted */ uint32 ChangedOrRemovedPakCount; /** Our passed-in World */ TWeakObjectPtr OwnerWorld; /** Loaded hotfix contents that were not mapped to any known branch, but might be loaded later */ TMap>> DynamicHotfixContents; UE_API virtual void Init(); UE_API virtual void Cleanup(); /** Looks at each file returned via the hotfix and processes them */ UE_API EHotfixResult ApplyHotfix(); /** Cleans up and fires the delegate indicating it's done */ UE_API void TriggerHotfixComplete(EHotfixResult HotfixResult); /** Checks each file listed to see if it is a hotfix file to process */ UE_API void FilterHotfixFiles(); /** Starts the async reading process for the hotfix files */ UE_API void ReadHotfixFiles(); /** Unmounts any changed PAK files so they can be re-mounted after downloading */ UE_API void UnmountHotfixFiles(); /** Stores off the INI file for restoration later */ UE_API FConfigFileBackup& BackupIniFile(const FString& IniName, const FConfigFile* ConfigFile); /** Restores any changed INI files to their default loaded state */ UE_API void RestoreBackupIniFiles(); /** Builds the list of files that are different between two runs of the hotfix process */ UE_API void BuildHotfixFileListDeltas(); /** Called once the list of hotfix files has been retrieved */ UE_API void OnEnumerateFilesComplete(bool bWasSuccessful, const FString& ErrorStr); /** Called once the list of hotfix files has been retrieved and we only want to see if a hotfix is necessary */ UE_API void OnEnumerateFilesForAvailabilityComplete(bool bWasSuccessful, const FString& ErrorStr, FOnHotfixAvailableComplete InCompletionDelegate); /** Called as files are downloaded to determine when to apply the hotfix data */ UE_API void OnReadFileComplete(bool bWasSuccessful, const FString& FileName); /** Called as files are downloaded to provide progress notifications */ UE_API void OnReadFileProgress(const FString& FileName, uint64 BytesRead); /** @return the config file entry for the ini file name in question */ UE_API FConfigFile* GetConfigFile(const FString& IniName); UE_API FConfigBranch* GetBranch(const FString& IniName); /** @return the config cache key used to associate ini file entries within the config cache */ UE_API FString BuildConfigCacheKey(const FString& IniName); /** @return the config file name after stripping any extra info (platform, debug prefix, etc.) */ UE_API virtual FString GetStrippedConfigFileName(const FString& IniName); /** @return the human readable name of the file */ UE_API const FString GetFriendlyNameFromDLName(const FString& DLName) const; UE_API virtual void PostInitProperties() override; UE_API bool IsMapLoaded(const FString& MapName); /** @return our current world */ UE_API UWorld* GetWorld() const override; /** Stop tracking hotfixed assets marked as garbage */ UE_API void StopTrackingInvalidHotfixedAssets(); /** Hotfix a dynamic config branch that was just loaded */ UE_API void HotfixDynamicBranch(const FName& Tag, const FName& Branch, class FConfigModificationTracker* ModificationTracker); protected: /** * Is this hotfix file compatible with the current build * If the file has version information it is compared with compatibility * If the file has NO version information it is assumed compatible * * @param InFilename name of the file to check * @param OutFilename name of file with version information stripped * * @return true if file is compatible, false otherwise */ UE_API bool IsCompatibleHotfixFile(const FString& InFilename, FString& OutFilename); /** * Override this method to look at the file information for any game specific hotfix processing * NOTE: Make sure to call Super to get default handling of files * * @param FileHeader - the information about the file to determine if it needs custom processing * * @return true if the file needs some kind of processing, false to have hotfixing ignore the file */ UE_API virtual bool WantsHotfixProcessing(const FCloudFileHeader& FileHeader); /** * Called when a file needs custom processing (see above). Override this to provide your own processing methods * * @param FileHeader - the header information for the file in question * * @return whether the file was successfully processed */ UE_API virtual bool ApplyHotfixProcessing(const FCloudFileHeader& FileHeader); /** * Called prior to reading the file data. * * @param FileHeader - the header information for the file in question * @param FileData - byte data of the hotfix file. Intentionally not const, so the array is modifiable as part of preprocessing. * * @return whether the file was successfully preprocessed */ virtual bool PreProcessDownloadedFileData(const FCloudFileHeader& FileHeader, TArray& FileData) const { return true; } /** * Override this to change the default INI file handling (merge delta INI changes into the config cache) * * @param FileName - the name of the INI being merged into the config cache * @param IniData - the contents of the INI file (expected to be delta contents not a whole file) * * @return whether the merging was successful or not */ UE_API virtual bool HotfixIniFile(const FString& FileName, const FString& IniData); /** * Override this to change the default PAK file handling: * - mount PAK file immediately * - Scan for any INI files contained within the PAK file and merge those in * * @param FileName - the name of the PAK file being mounted * * @return whether the mounting of the PAK file was successful or not */ UE_API virtual bool HotfixPakFile(const FCloudFileHeader& FileHeader); /** * Override this to change the default INI file handling (merge whole INI files into the config cache) * * @param FileName - the name of the INI being merged into the config cache * * @return whether the merging was successful or not */ UE_API virtual bool HotfixPakIniFile(const FString& FileName); /** * Override this to change the default caching directory * * @return the default caching directory */ virtual FString GetCachedDirectory() { return FPaths::ProjectPersistentDownloadDir(); } /** Notify used by CheckAvailability() */ UE_API virtual void OnHotfixAvailablityCheck(const TArray& PendingChangedFiles, const TArray& PendingRemoveFiles); /** Finds the header associated with the file name */ UE_API FCloudFileHeader* GetFileHeaderFromDLName(const FString& FileName); /** Fires the progress delegate with our updated progress */ UE_API void UpdateProgress(uint32 FileCount, uint64 UpdateSize); virtual bool ShouldWarnAboutMissingWhenPatchingFromIni(const FString& AssetPath) const { return true; } /** Called after any hotfixes are applied to apply last-second changes to certain asset types from .ini file data */ UE_API virtual void PatchAssetsFromIniFiles(); /** Called after any hotfixes are applied to apply last-second changes to Config properties from .ini file data */ UE_API virtual void ReloadConfigsFromIniFiles(); /** Used in PatchAssetsFromIniFiles to hotfix only a row in a table. * If ChangedTables is not null then HandleDataTableChanged will not be called and the caller should call it on the data tables in ChangedTables when they're ready to */ UE_API void HotfixRowUpdate( UObject* Asset, const FString& AssetPath, const FString& RowName, const FString& ColumnName, const FString& NewValue, TArray& ProblemStrings, TSet* ChangedDataTables = nullptr, TSet* ChangedCurveTables = nullptr); /** Used in PatchAssetsFromIniFiles to hotfix a new row in a table. * If ChangedTables is not null then HandleDataTableChanged will not be called and the caller should call it on the data tables in ChangedTables when they're ready to */ UE_API void HotfixAddRow( UObject* Asset, const FString& AssetPath, const FString& JsonData, TArray& ProblemStrings, TSet* ChangedDataTables = nullptr); /** Called after adding table row by HotfixAddRow() */ virtual void OnHotfixTableAddRow(UObject& Asset, const FName RowName) {} /** Used in PatchAssetsFromIniFiles to hotfix an entire table. */ UE_API void HotfixTableUpdate(UObject* Asset, const FString& AssetPath, const FString& JsonData, TArray& ProblemStrings); /** Called after modifying table values by HotfixRowUpdate() */ virtual void OnHotfixTableValueInt64(UObject& Asset, const FString& RowName, const FString& ColumnName, const int64& OldValue, const int64& NewValue) { } virtual void OnHotfixTableValueDouble(UObject& Asset, const FString& RowName, const FString& ColumnName, const double& OldValue, const double& NewValue) { } virtual void OnHotfixTableValueFloat(UObject& Asset, const FString& RowName, const FString& ColumnName, const float& OldValue, const float& NewValue) { } virtual void OnHotfixTableValueString(UObject& Asset, const FString& RowName, const FString& ColumnName, const FString& OldValue, const FString& NewValue) { } virtual void OnHotfixTableValueName(UObject& Asset, const FString& RowName, const FString& ColumnName, const FName& OldValue, const FName& NewValue) { } virtual void OnHotfixTableValueObject(UObject& Asset, const FString& RowName, const FString& ColumnName, const UObject* OldValue, const UObject* NewValue) { } virtual void OnHotfixTableValueSoftObject(UObject& Asset, const FString& RowName, const FString& ColumnName, const FSoftObjectPtr& OldValue, const FSoftObjectPtr& NewValue) { } UE_API virtual bool ShouldPerformHotfix(); /** Allow the application to override the dedicated server filename prefix. */ UE_API virtual FString GetDedicatedServerPrefix() const; /** Allow child classes to determine if specific assets should be hotfixed or not */ UE_API virtual bool ShouldHotfixAsset(const FString& AssetPath) const; #if !UE_BUILD_SHIPPING /** Test function that applies a local file as if it were a hotfix. */ UE_API void ApplyLocalTestHotfix(FString Filename); #endif public: UE_API UOnlineHotfixManager(); UE_API UOnlineHotfixManager(FVTableHelper& Helper); UE_API virtual ~UOnlineHotfixManager(); /** Tells the hotfix manager which OSS to use. Uses the default if empty */ UPROPERTY(Config) FString OSSName; /** Tells the factory method which class to contruct */ UPROPERTY(Config) FString HotfixManagerClassName; /** Used to prevent development work from interfering with playtests, etc. */ UPROPERTY(Config) FString DebugPrefix; /** Starts the fetching of hotfix data from the OnlineTitleFileInterface that is registered for this game */ UFUNCTION(BlueprintCallable, Category="Hotfix") UE_API virtual void StartHotfixProcess(); /** Array of objects that we're forcing to remain resident because we've applied live hotfixes and won't get an opportunity to reapply changes if the object is evicted from memory. */ UPROPERTY(Transient) TArray> AssetsHotfixedFromIniFiles; /** * Check for available hotfix files (but do not apply them) * * @param InCompletionDelegate delegate to fire when the check is complete */ UE_API virtual void CheckAvailability(FOnHotfixAvailableComplete& InCompletionDelegate); /** Factory method that returns the configured hotfix manager */ static UE_API UOnlineHotfixManager* Get(UWorld* World); static UE_API void ReloadObjectsAffectedByConfigFile(const FString& IniDataFileName, const FString& IniData, const FString& ConfigFilename, TArray& ReloadedClassesPathNames, bool bUseLoadConfig); }; #undef UE_API