From 61f50e3c95bf1c2da4d6175abef00066bcc02187 Mon Sep 17 00:00:00 2001 From: Paul Hazen Date: Thu, 24 Oct 2024 12:54:11 -0700 Subject: [PATCH 1/4] fix: Add isolated changes that address the Android thread IO issue. --- .../Android/Core/AndroidFileIOHelper.cs | 72 +++--- .../Runtime/Core/Config/Config.cs | 216 ++++++++++++------ .../Runtime/Core/Config/EOSConfig.cs | 2 +- .../Runtime/Core/Config/PlatformConfig.cs | 9 +- .../Runtime/Core/EOSManager.cs | 55 ++--- .../Runtime/Core/PlatformSpecifics.cs | 4 +- .../Runtime/Core/Utility/FileSystemUtility.cs | 139 ++++++----- 7 files changed, 314 insertions(+), 183 deletions(-) diff --git a/Assets/Plugins/Android/Core/AndroidFileIOHelper.cs b/Assets/Plugins/Android/Core/AndroidFileIOHelper.cs index fa13a4a00..43101db49 100644 --- a/Assets/Plugins/Android/Core/AndroidFileIOHelper.cs +++ b/Assets/Plugins/Android/Core/AndroidFileIOHelper.cs @@ -1,45 +1,65 @@ /* - * Copyright (c) 2024 PlayEveryWare - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ +* Copyright (c) 2024 PlayEveryWare +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ namespace PlayEveryWare.EpicOnlineServices { using System; + using System.IO; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; public class AndroidFileIOHelper { - public static async Task ReadAllText(string filePath) + public static bool FileExists(string filePath) { using UnityWebRequest request = UnityWebRequest.Get(filePath); - request.timeout = 2; //seconds till timeout - UnityWebRequestAsyncOperation operation = request.SendWebRequest(); + request.timeout = 2; + request.SendWebRequest(); + while (!request.isDone) { } + + bool exists = (request.result == UnityWebRequest.Result.Success); - while (!operation.isDone) + if (!exists) { - await Task.Yield(); + Debug.LogError($"AndroidFileIOHelper says that \"{filePath}\" does not exist."); } + return exists; + } + + public static string ReadAllText(string filePath) + { + using UnityWebRequest request = UnityWebRequest.Get(filePath); + request.timeout = 2; //seconds till timeout + request.SendWebRequest(); + + while (!request.isDone) { } + + return ProcessRequest(filePath, request); + } + + private static string ProcessRequest(string filePath, UnityWebRequest request) + { string text = null; switch (request.result) @@ -72,4 +92,4 @@ public static async Task ReadAllText(string filePath) return text; } } -} +} \ No newline at end of file diff --git a/com.playeveryware.eos/Runtime/Core/Config/Config.cs b/com.playeveryware.eos/Runtime/Core/Config/Config.cs index 37be568ec..5e6c658bf 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Config.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/Config.cs @@ -22,6 +22,7 @@ namespace PlayEveryWare.EpicOnlineServices { + using Newtonsoft.Json; using System; using System.Linq; using System.Threading.Tasks; @@ -29,7 +30,7 @@ namespace PlayEveryWare.EpicOnlineServices using UnityEngine; using System.Collections.Generic; using System.IO; - using System.Reflection; + using System.Reflection; using System.Text; using JsonUtility = PlayEveryWare.EpicOnlineServices.Utility.JsonUtility; using System.Runtime.CompilerServices; @@ -76,6 +77,13 @@ public abstract class Config /// private string _lastReadJsonString; + /// + /// Indicates whether, if the file is not found, it is acceptable to + /// return from the Get functions an instance of the config with + /// default values. + /// + private readonly bool _allowDefaultIfFileNotFound; + /// /// Instantiate a new config based on the file at the given filename - /// in a default directory. @@ -83,9 +91,15 @@ public abstract class Config /// /// The name of the file containing the config values. /// - protected Config(string filename) : + /// + /// Indicates whether, if the backing file cannot be found it is + /// acceptable to return from the Get functions an instance of the + /// config with default values. + /// + protected Config(string filename, bool allowDefault = false) : this(filename, FileSystemUtility.CombinePaths( - Application.streamingAssetsPath, "EOS")) { } + Application.streamingAssetsPath, "EOS"), allowDefault) + { } /// /// Instantiates a new config based on the file at the given file and @@ -97,10 +111,19 @@ protected Config(string filename) : /// /// The directory that contains the file. /// - protected Config(string filename, string directory) + /// + /// Indicates whether, if the backing file cannot be found, it is + /// acceptable to return from the Get functions an instance of the + /// config with default values. + /// + protected Config( + string filename, + string directory, + bool allowDefault = false) { Filename = filename; Directory = directory; + _allowDefaultIfFileNotFound = allowDefault; } /// @@ -111,7 +134,7 @@ protected Config(string filename, string directory) /// /// The config type. /// The function to create the config type - protected static void RegisterFactory(Func factory) + protected static void RegisterFactory(Func factory) where T : Config { s_factories[typeof(T)] = factory; @@ -136,30 +159,37 @@ protected static void RegisterFactory(Func factory) /// how to properly implement the Config implementing class such that /// its constructor is properly registered. /// - private static bool TryGetFactory(out Func factory) + private static bool TryGetFactory(out Func factory) where T : Config { // Ensure static constructor of template variable type is called RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle); - if(!s_factories.TryGetValue(typeof(T), out factory)) + if (!s_factories.TryGetValue(typeof(T), out factory)) { throw new InvalidOperationException( $"No factory method has been registered for " + $"type \"{typeof(T).FullName}\". " + $"Please make sure that \"{typeof(T).FullName}\" " + - $"registers its constructor with the base Config class " + - $"via a static constructor."); + "registers its constructor with the base Config class via" + + " a static constructor."); } return true; } + // NOTE: This compile conditional is here because in Unity, Async IO + // works poorly on Android devices. +#if !UNITY_ANDROID || UNITY_EDITOR /// /// Retrieves indicated Config object, reading its values into memory. /// - /// The Config to retrieve. - /// TaskConfig type. + /// + /// The Config to retrieve. + /// + /// + /// TaskConfig type. + /// public static async Task GetAsync() where T : Config { // NOTE: This block (and the corresponding one below) exists so that @@ -174,7 +204,7 @@ public static async Task GetAsync() where T : Config } #endif // Try to get the factory method used to instantiate the config. - TryGetFactory(out Func factory); + _ = TryGetFactory(out Func factory); // Use the factory method to create the config. T instance = (T)factory(); @@ -190,6 +220,7 @@ public static async Task GetAsync() where T : Config // Return the config being retrieved. return instance; } +#endif /// /// Retrieves indicated Config object, reading its values into memory. @@ -198,62 +229,40 @@ public static async Task GetAsync() where T : Config /// TaskConfig type. public static T Get() where T : Config { - T config = Task.Run(GetAsync).GetAwaiter().GetResult(); - return config; - } + // NOTE: This block (and the corresponding one below) exists so that + // the config values are only cached when not in the editor. + // In the editor, config files can be changed, so they should + // not be cached. +#if !UNITY_EDITOR + // Return cached copy if it exists. + if (s_cachedConfigs.TryGetValue(typeof(T), out Config config)) + { + return (T)config; + } +#endif + // Try to get the factory method used to instantiate the config. + _ = TryGetFactory(out Func factory); - /// - /// This delegate describes the signature of a function that can be used - /// to convert a list of strings into a single enum value. - /// - /// - /// The type of enum to convert the list of strings to a value of. - /// - /// - /// Strings to convert into an enum value. - /// - /// - /// The enum value that results from performing a bitwise OR operation - /// on the list of enum values that result from converting each item in - /// the provided list of strings to an enum value of the indicated type. - /// - /// - /// True if the parsing of flags was successful, false otherwise. - /// - protected delegate bool TryParseEnumDelegate(IList - stringFlags, out TEnum result) where TEnum : struct, Enum; + // Use the factory method to create the config. + T instance = (T)factory(); - /// - /// Private static wrapper to handle converting a list of strings into - /// a single enum value. - /// - /// - /// The type of enum to convert the list of strings into. - /// - /// - /// The list of strings to convert into a single enum value. - /// - /// - /// The function used to convert the list of strings into a single enum - /// value. - /// - /// A single enum value that is the result of a bitwise OR - /// operation between the enum values that result from parsing each of - /// the items in a list into the indicated enum type value. - /// - protected static TEnum StringsToEnum( - IList stringFlags, - TryParseEnumDelegate parseEnumFn) - where TEnum : struct, Enum - { - _ = parseEnumFn(stringFlags, out TEnum result); - return result; + // Synchronously read config values from the corresponding file. + instance.Read(); + +#if !UNITY_EDITOR + // Cache the newly created config with its values having been read. + s_cachedConfigs.Add(typeof(T), instance); +#endif + + // Return the config being retrieved. + return instance; } /// /// Returns the fully-qualified path to the file that holds the /// configuration values. /// + [JsonIgnore] public string FilePath { get @@ -262,6 +271,9 @@ public string FilePath } } + // NOTE: This compile conditional is here because Async IO does not work + // well on Android. +#if !UNITY_ANDROID || UNITY_EDITOR /// /// Asynchronously read the values from the JSON file associated with /// this Config @@ -270,9 +282,14 @@ public string FilePath protected virtual async Task ReadAsync() { await EnsureConfigFileExistsAsync(); - _lastReadJsonString = await FileSystemUtility.ReadAllTextAsync(FilePath); - JsonUtility.FromJsonOverwrite(_lastReadJsonString, this); + + if (await FileSystemUtility.FileExistsAsync(FilePath)) + { + _lastReadJsonString = await FileSystemUtility.ReadAllTextAsync(FilePath); + JsonUtility.FromJsonOverwrite(_lastReadJsonString, this); + } } +#endif /// /// Synchronously reads the contents of a Config from the json file that @@ -280,8 +297,13 @@ protected virtual async Task ReadAsync() /// protected virtual void Read() { - // Call ReadAsync() synchronously. - Task.Run(ReadAsync).GetAwaiter().GetResult(); + if (!FileSystemUtility.FileExists(FilePath)) + { + return; + } + + _lastReadJsonString = FileSystemUtility.ReadAllText(FilePath); + JsonUtility.FromJsonOverwrite(_lastReadJsonString, this); } /// @@ -298,6 +320,8 @@ private async Task EnsureConfigFileExistsAsync() #if UNITY_EDITOR await WriteAsync(); #else + if (_allowDefaultIfFileNotFound) + return; // If the editor is not running, then the config file not // existing should throw an error. throw new FileNotFoundException( @@ -321,7 +345,7 @@ private async Task EnsureConfigFileExistsAsync() /// /// Task public virtual async Task WriteAsync( - bool prettyPrint = true, + bool prettyPrint = true, bool updateAssetDatabase = true) { var json = JsonUtility.ToJson(this, prettyPrint); @@ -344,7 +368,7 @@ public virtual async Task WriteAsync( /// Indicates whether to update the asset database after writing. /// public virtual void Write( - bool prettyPrint = true, + bool prettyPrint = true, bool updateAssetDatabase = true) { var json = JsonUtility.ToJson(this, prettyPrint); @@ -419,7 +443,7 @@ private static bool IsDefault(T configInstance) where T : Config */ return IteratePropertiesAndFields(configInstance) - .All(mInfo => + .All(mInfo => GetDefaultValue(mInfo.MemberType) == mInfo.MemberValue); } @@ -545,10 +569,10 @@ public bool Equals(MemberInfo a, MemberInfo b) { // consider the member values to be equal if one is null and // the other is empty - return ((a.MemberValue == null && - ((List)b.MemberValue).Count == 0) + return ((a.MemberValue == null && + ((List)b.MemberValue).Count == 0) || - (((List)a.MemberValue).Count == 0 && + (((List)a.MemberValue).Count == 0 && b.MemberValue == null)); } else if (a.MemberType == typeof(string)) @@ -567,11 +591,59 @@ public bool Equals(MemberInfo a, MemberInfo b) public int GetHashCode(MemberInfo memberInfo) { return HashCode.Combine( - memberInfo.MemberType, + memberInfo.MemberType, memberInfo.MemberValue); } } + /// + /// This delegate describes the signature of a function that can be used + /// to convert a list of strings into a single enum value. + /// + /// + /// The type of enum to convert the list of strings to a value of. + /// + /// + /// Strings to convert into an enum value. + /// + /// + /// The enum value that results from performing a bitwise OR operation + /// on the list of enum values that result from converting each item in + /// the provided list of strings to an enum value of the indicated type. + /// + /// + /// True if the parsing of flags was successful, false otherwise. + /// + protected delegate bool TryParseEnumDelegate(IList + stringFlags, out TEnum result) where TEnum : struct, Enum; + + /// + /// Private static wrapper to handle converting a list of strings into + /// a single enum value. + /// + /// + /// The type of enum to convert the list of strings into. + /// + /// + /// The list of strings to convert into a single enum value. + /// + /// + /// The function used to convert the list of strings into a single enum + /// value. + /// + /// A single enum value that is the result of a bitwise OR + /// operation between the enum values that result from parsing each of + /// the items in a list into the indicated enum type value. + /// + protected static TEnum StringsToEnum( + IList stringFlags, + TryParseEnumDelegate parseEnumFn) + where TEnum : struct, Enum + { + _ = parseEnumFn(stringFlags, out TEnum result); + return result; + } + /// /// Gets an IEnumerable of the type / value pairs for each Field or /// Property matching the given BindingFlags on the given instance. @@ -591,7 +663,7 @@ public int GetHashCode(MemberInfo memberInfo) /// private static IEnumerable IteratePropertiesAndFields( T instance, - BindingFlags bindingAttr = + BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Instance) { // go over the properties diff --git a/com.playeveryware.eos/Runtime/Core/Config/EOSConfig.cs b/com.playeveryware.eos/Runtime/Core/Config/EOSConfig.cs index 7b02978c0..a856b2d69 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/EOSConfig.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/EOSConfig.cs @@ -440,7 +440,7 @@ private static bool TryGetDeployment( public PlatformFlags GetPlatformFlags() { return StringsToEnum( - platformOptionsFlags, + platformOptionsFlags, PlatformFlagsExtensions.TryParse); } diff --git a/com.playeveryware.eos/Runtime/Core/Config/PlatformConfig.cs b/com.playeveryware.eos/Runtime/Core/Config/PlatformConfig.cs index 8c79bb012..726e034e2 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/PlatformConfig.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/PlatformConfig.cs @@ -24,6 +24,7 @@ namespace PlayEveryWare.EpicOnlineServices { #if !EOS_DISABLE using Epic.OnlineServices.IntegratedPlatform; + using Utility; #endif using Extensions; using System; @@ -77,10 +78,10 @@ protected PlatformConfig(PlatformManager.Platform platform) : #if !EOS_DISABLE public IntegratedPlatformManagementFlags GetIntegratedPlatformManagementFlags() { - return StringsToEnum( - flags, - IntegratedPlatformManagementFlagsExtensions.TryParse - ); + _ = IntegratedPlatformManagementFlagsExtensions.TryParse(flags, + out IntegratedPlatformManagementFlags flagsEnum); + + return flagsEnum; } #endif } diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index 2b74f0938..19d65362c 100644 --- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs +++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs @@ -51,6 +51,7 @@ namespace PlayEveryWare.EpicOnlineServices { + using Extensions; using UnityEngine; using System; using System.Collections.Generic; @@ -179,7 +180,7 @@ public partial class EOSSingleton static private NotifyEventHandle s_notifyLoginStatusChangedCallbackHandle; static private NotifyEventHandle s_notifyConnectLoginStatusChangedCallbackHandle; static private NotifyEventHandle s_notifyConnectAuthExpirationCallbackHandle; - + // Setting it twice will cause an exception static bool hasSetLoggingCallback; @@ -469,13 +470,14 @@ private PlatformInterface CreatePlatformInterface() EOSCreateOptions platformOptions = new EOSCreateOptions(); + platformOptions.options.CacheDirectory = platformSpecifics.GetTempDir(); platformOptions.options.IsServer = configData.isServer; platformOptions.options.Flags = #if UNITY_EDITOR PlatformFlags.LoadingInEditor; #else - configData.GetPlatformFlags(); + configData.platformOptionsFlags.Unwrap(); #endif if (configData.IsEncryptionKeyValid()) { @@ -564,8 +566,7 @@ public void Init(IEOSCoroutineOwner coroutineOwner) Init(coroutineOwner, EOSPackageInfo.ConfigFileName); } - //------------------------------------------------------------------------- - public void Init(IEOSCoroutineOwner coroutineOwner, string configFileName) + private void Init(IEOSCoroutineOwner coroutineOwner, string configFileName) { if (GetEOSPlatformInterface() != null) { @@ -756,6 +757,12 @@ public void SetLogLevel(LogCategory Category, LogLevel Level) /// private void InitializeLogLevels() { + // This compile conditional is here to circumnavigate issues + // unique to android with respect to Config class functionality. +#if UNITY_ANDROID && !UNITY_EDITOR + SetLogLevel(LogCategory.AllCategories, LogLevel.Info); + return; +#else var logLevelList = LogLevelUtility.LogLevelList; if (logLevelList == null) @@ -768,6 +775,7 @@ private void InitializeLogLevels() { SetLogLevel((LogCategory)logCategoryIndex, logLevelList[logCategoryIndex]); } +#endif } //------------------------------------------------------------------------- @@ -832,15 +840,19 @@ static private LoginOptions MakeLoginOptions(LoginCredentialType loginType, Token = token }; - var defaultScopeFlags = - AuthScopeFlags.BasicProfile | AuthScopeFlags.FriendsList | AuthScopeFlags.Presence; + AuthScopeFlags scopeFlags = (AuthScopeFlags.BasicProfile | + AuthScopeFlags.FriendsList | + AuthScopeFlags.Presence); + + if (Config.Get().GetAuthScopeFlags() != AuthScopeFlags.NoFlags) + { + scopeFlags = Config.Get().GetAuthScopeFlags(); + } return new LoginOptions { Credentials = loginCredentials, - ScopeFlags = Config.Get().authScopeOptionsFlags.Count > 0 - ? Config.Get().GetAuthScopeFlags() - : defaultScopeFlags + ScopeFlags = scopeFlags }; } @@ -1074,24 +1086,13 @@ public void StartConnectLoginWithEpicAccount(EpicAccountId epicAccountId, { print("Attempting to use refresh token to login with connect"); - // need to refresh the epicaccount id - // LoginCredentialType.RefreshToken - Instance.StartLoginWithLoginTypeAndToken(LoginCredentialType.RefreshToken, null, - authToken.Value.RefreshToken, callbackInfo => - { - var EOSAuthInterface = GetEOSPlatformInterface().GetAuthInterface(); - var copyUserTokenOptions = new CopyUserAuthTokenOptions(); - var result = EOSAuthInterface.CopyUserAuthToken(ref copyUserTokenOptions, - callbackInfo.LocalUserId, out Token? userAuthToken); - - connectLoginOptions.Credentials = new Epic.OnlineServices.Connect.Credentials - { - Token = userAuthToken.Value.AccessToken, - Type = ExternalCredentialType.Epic - }; + connectLoginOptions.Credentials = new Epic.OnlineServices.Connect.Credentials + { + Token = authToken.Value.RefreshToken, + Type = ExternalCredentialType.Epic + }; - StartConnectLoginWithOptions(connectLoginOptions, onConnectLoginCallback); - }); + StartConnectLoginWithOptions(connectLoginOptions, onConnectLoginCallback); } else if (authToken.Value.AccessToken != null) { @@ -1516,7 +1517,7 @@ public void RemovePersistentToken() if (deletePersistentAuthCallbackInfo.ResultCode != Result.Success) { print("Unable to delete persistent token, Result : " + - deletePersistentAuthCallbackInfo.ResultCode, + deletePersistentAuthCallbackInfo.ResultCode, LogType.Error); } else diff --git a/com.playeveryware.eos/Runtime/Core/PlatformSpecifics.cs b/com.playeveryware.eos/Runtime/Core/PlatformSpecifics.cs index 887238022..7fd0b33f7 100644 --- a/com.playeveryware.eos/Runtime/Core/PlatformSpecifics.cs +++ b/com.playeveryware.eos/Runtime/Core/PlatformSpecifics.cs @@ -50,7 +50,7 @@ public string GetDynamicLibraryExtension() } #endregion - + #region Virtual methods that have a default behavior, but may need to be overwritten by deriving classes. public virtual string GetTempDir() @@ -108,7 +108,7 @@ public virtual void ConfigureSystemInitOptions(ref EOSInitializeOptions initiali Debug.Log($"Assigning thread affinity override values for platform \"{Platform}\"."); var overrideThreadAffinity = initializeOptions.options.OverrideThreadAffinity.Value; - Config.Get().overrideValues.ConfigureOverrideThreadAffinity(ref overrideThreadAffinity); + Config.Get().ConfigureOverrideThreadAffinity(ref overrideThreadAffinity); initializeOptions.options.OverrideThreadAffinity = overrideThreadAffinity; } diff --git a/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs b/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs index e64b00e47..d3b0a659f 100644 --- a/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs +++ b/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs @@ -52,6 +52,12 @@ namespace PlayEveryWare.EpicOnlineServices.Utility /// internal static class FileSystemUtility { + // This compile conditional exists because the following functions + // make use of the System.Linq namespace which is undesirable to use + // during runtime. Since these functions are currently only ever + // utilized in areas of the code that run in the editor, it is + // appropriate to use compile conditionals to include / exclude them. +#if UNITY_EDITOR /// /// Interval with which to update progress, in milliseconds /// @@ -125,7 +131,7 @@ public class CopyFileProgressInfo public static bool TryGetTempDirectory(out string path) { path = default; - + // Nested local function to reduce repetitive code. string GenerateTempPath() { @@ -186,12 +192,7 @@ string GenerateTempPath() return true; } - // This compile conditional exists because the following functions - // make use of the System.Linq namespace which is undesirable to use - // during runtime. Since these functions are currently only ever - // utilized in areas of the code that run in the editor, it is - // appropriate to use compile conditionals to include / exclude them. -#if UNITY_EDITOR + /// /// Get a list of the directories that are represented by the filepaths /// provided. The list is unique, and is ordered by smallest path first, @@ -207,7 +208,7 @@ string GenerateTempPath() /// /// public static IEnumerable GetDirectories( - IEnumerable filepaths, + IEnumerable filepaths, bool creationOrder = true) { // For each filepath, determine the immediate parent directory of @@ -219,9 +220,9 @@ public static IEnumerable GetDirectories( // skip if no parent if (null == parent) continue; - + directoriesToCreate.Add(parent); - + } // Return the list of directories to create in ascending order of @@ -258,9 +259,9 @@ public static IEnumerable GetDirectories( /// Cancellation token. /// Task public static async Task CopyFilesAsync( - IList operations, - CancellationToken cancellationToken = default, - IProgress progress = null, + IList operations, + CancellationToken cancellationToken = default, + IProgress progress = null, int updateIntervalMS = DefaultUpdateIntervalMS) { IEnumerable directoriesToCreate = GetDirectories( @@ -298,9 +299,9 @@ public static async Task CopyFilesAsync( // Copy the files asynchronously with the provided // cancellation token, and progress stuff. await CopyFilesAsyncInternal( - operationsList, - cancellationToken, - progress, + operationsList, + cancellationToken, + progress, progressInfo); } else @@ -318,9 +319,9 @@ await CopyFilesAsyncInternal( /// Progress information. /// Task private static async Task CopyFilesAsyncInternal( - IEnumerable operations, - CancellationToken cancellationToken = default, - IProgress progress = null, + IEnumerable operations, + CancellationToken cancellationToken = default, + IProgress progress = null, CopyFileProgressInfo progressInfo = default) { var tasks = operations.Select(async copyOperation => @@ -349,7 +350,7 @@ private static async Task CopyFilesAsyncInternal( await Task.WhenAll(tasks); } -#endif + /// /// Copies a single file asynchronously. /// @@ -357,14 +358,14 @@ private static async Task CopyFilesAsyncInternal( /// Cancellation token. /// Task private static async Task CopyFileAsync( - CopyFileOperation op, + CopyFileOperation op, CancellationToken cancellationToken) { // Maximum number of times the operation is retried if it fails. const int maxRetries = 3; // This is the initial delay before the operation is retried. - const int delayMilliseconds = 200; + const int delayMilliseconds = 200; for (int attempt = 0; attempt < maxRetries; attempt++) { @@ -397,7 +398,7 @@ await Task.Run(() => // exponentially increase the delay to maximize the chance // it will succeed without waiting too long. var delay = delayMilliseconds * (int)Math.Pow(2, attempt); - + // Construct detailed message regarding the nature of the problem. StringBuilder sb = new(); sb.AppendLine($"Exception occurred during the following copy operation:"); @@ -413,7 +414,6 @@ await Task.Run(() => } } -#if UNITY_EDITOR /// /// Returns the root of the Unity project. /// @@ -479,10 +479,12 @@ public static void ConvertDosToUnixLineEndings(string srcFilename, string destFi #endif #region File Read Functionality - + // NOTE: This compile conditional is here because on Android devices + // async IO doesn't work well. +#if !UNITY_ANDROID || UNITY_EDITOR public static async Task<(bool Success, string Result)> TryReadAllTextAsync(string filePath) { - bool fileExists = await ExistsInternal(filePath, false); + bool fileExists = await ExistsInternalAsync(filePath, false); if (!fileExists) { @@ -494,15 +496,6 @@ public static void ConvertDosToUnixLineEndings(string srcFilename, string destFi return null == contents ? (false, null) : (true, contents); } - /// - /// Reads all text from the indicated file. - /// - /// Filepath to the file to read from. - /// The contents of the file at the indicated path as a string. - public static string ReadAllText(string path) - { - return Task.Run(() => ReadAllTextAsync(path)).GetAwaiter().GetResult(); - } /// /// Asynchronously reads all text from the indicated file. @@ -511,15 +504,31 @@ public static string ReadAllText(string path) /// Task public static async Task ReadAllTextAsync(string path) { + try + { + return await File.ReadAllTextAsync(path); + } + catch (Exception e) + { + Debug.LogException(e); + throw; + } + } +#endif + + /// + /// Reads all text from the indicated file. + /// + /// Filepath to the file to read from. + /// The contents of the file at the indicated path as a string. + public static string ReadAllText(string path) + { #if UNITY_ANDROID && !UNITY_EDITOR - // On Android, use a custom helper to read the file - return await AndroidFileIOHelper.ReadAllText(path); + return AndroidFileIOHelper.ReadAllText(path); #else - // On other platforms, read asynchronously or synchronously as - // appropriate. try { - return await File.ReadAllTextAsync(path); + return File.ReadAllText(path); } catch (Exception e) { @@ -527,8 +536,11 @@ public static async Task ReadAllTextAsync(string path) throw; } #endif + } + + #endregion #region Get File System Entries Functionality @@ -558,7 +570,6 @@ public static IEnumerable GetFileSystemEntries(string path, string patte #endregion - #region Path Functionality /// @@ -662,7 +673,7 @@ private static void CreateDirectory(DirectoryInfo dInfo) { dInfo.Create(); } - catch(Exception ex) + catch (Exception ex) { Debug.LogException(ex); } @@ -674,25 +685,51 @@ private static void CreateDirectory(DirectoryInfo dInfo) public static bool DirectoryExists(string path) { - return DirectoryExistsAsync(path).GetAwaiter().GetResult(); + return ExistsInternal(path, true); } - public static async Task DirectoryExistsAsync(string path) + public static bool FileExists(string path) { - return await ExistsInternal(path, isDirectory: true); + return ExistsInternal(path); } - public static bool FileExists(string path) + private static bool ExistsInternal(string path, bool isDirectory = false) + { + bool exists = false; + +#if UNITY_ANDROID && !UNITY_EDITOR + if (isDirectory) + { + throw new Exception("Cannot determine if directory exists in Android."); + } + + return AndroidFileIOHelper.FileExists(path); +#else + if (isDirectory) + { + exists = Directory.Exists(path); + } + else + { + exists = File.Exists(path); + } +#endif + + return exists; + } + + public static async Task DirectoryExistsAsync(string path) { - return FileExistsAsync(path).GetAwaiter().GetResult(); + return await ExistsInternalAsync(path, isDirectory: true); } + public static async Task FileExistsAsync(string path) { - return await ExistsInternal(path, isDirectory: false); + return await ExistsInternalAsync(path, isDirectory: false); } - private static async Task ExistsInternal(string path, bool isDirectory) + private static async Task ExistsInternalAsync(string path, bool isDirectory) { bool exists = false; #if UNITY_ANDROID && !UNITY_EDITOR @@ -741,7 +778,7 @@ public static void CleanDirectory(string directoryPath, bool ignoreGit = true) { // Skip .git directories if (ignoreGit && subDir.EndsWith(".git")) { continue; } - + // TODO: This is a little bit dangerous as one developer has found out. If the output directory is not // empty, and contains directories and files unrelated to output, this will (without prompting) // delete them. So, if you're outputting to, say the "Desktop" directory, it will delete everything @@ -797,4 +834,4 @@ public static void OpenDirectory(string path) } #endif } -} +} \ No newline at end of file From 7adc64b93da9cda551afc52ce698b631c9ba76f2 Mon Sep 17 00:00:00 2001 From: Paul Hazen Date: Thu, 24 Oct 2024 13:38:43 -0700 Subject: [PATCH 2/4] fix: Correct usage of FileSystemUtility in the IOSBuilder. --- Assets/Plugins/Source/Editor/Platforms/iOS/IOSBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Plugins/Source/Editor/Platforms/iOS/IOSBuilder.cs b/Assets/Plugins/Source/Editor/Platforms/iOS/IOSBuilder.cs index ac08bed7b..569a640b4 100644 --- a/Assets/Plugins/Source/Editor/Platforms/iOS/IOSBuilder.cs +++ b/Assets/Plugins/Source/Editor/Platforms/iOS/IOSBuilder.cs @@ -22,6 +22,7 @@ #if !EOS_DISABLE namespace PlayEveryWare.EpicOnlineServices.Editor.Build { + using EpicOnlineServices.Utility; using System.IO; using UnityEditor; using UnityEditor.Build.Reporting; @@ -60,7 +61,7 @@ public override void PostBuild(BuildReport report) PBXProject proj = new(); - proj.ReadFromString(FileUtility.ReadAllText(projPath)); + proj.ReadFromString(FileSystemUtility.ReadAllText(projPath)); string targetGUID = proj.GetUnityMainTargetGuid(); string unityTargetGUID = proj.GetUnityFrameworkTargetGuid(); From f30ac52bd069c80024121216e096113cc56b5511 Mon Sep 17 00:00:00 2001 From: Paul Hazen Date: Thu, 24 Oct 2024 13:49:20 -0700 Subject: [PATCH 3/4] fix: Correct implementation for Android specifically - there were some errors in implementation. --- .../Runtime/Core/Config/Config.cs | 98 +++++++++---------- .../Runtime/Core/EOSManager.cs | 2 +- .../Runtime/Core/Utility/FileSystemUtility.cs | 2 +- 3 files changed, 50 insertions(+), 52 deletions(-) diff --git a/com.playeveryware.eos/Runtime/Core/Config/Config.cs b/com.playeveryware.eos/Runtime/Core/Config/Config.cs index 5e6c658bf..8279d9a8f 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Config.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/Config.cs @@ -330,6 +330,54 @@ private async Task EnsureConfigFileExistsAsync() } } + /// + /// This delegate describes the signature of a function that can be used + /// to convert a list of strings into a single enum value. + /// + /// + /// The type of enum to convert the list of strings to a value of. + /// + /// + /// Strings to convert into an enum value. + /// + /// + /// The enum value that results from performing a bitwise OR operation + /// on the list of enum values that result from converting each item in + /// the provided list of strings to an enum value of the indicated type. + /// + /// + /// True if the parsing of flags was successful, false otherwise. + /// + protected delegate bool TryParseEnumDelegate(IList + stringFlags, out TEnum result) where TEnum : struct, Enum; + + /// + /// Private static wrapper to handle converting a list of strings into + /// a single enum value. + /// + /// + /// The type of enum to convert the list of strings into. + /// + /// + /// The list of strings to convert into a single enum value. + /// + /// + /// The function used to convert the list of strings into a single enum + /// value. + /// + /// A single enum value that is the result of a bitwise OR + /// operation between the enum values that result from parsing each of + /// the items in a list into the indicated enum type value. + /// + protected static TEnum StringsToEnum( + IList stringFlags, + TryParseEnumDelegate parseEnumFn) + where TEnum : struct, Enum + { + _ = parseEnumFn(stringFlags, out TEnum result); + return result; + } + // Functions declared below should only ever be utilized in the editor. // They are so divided to guarantee separation of concerns. #if UNITY_EDITOR @@ -387,8 +435,6 @@ public virtual void Write( } } - - /// /// Determines whether the values in the Config have their /// default values @@ -596,54 +642,6 @@ public int GetHashCode(MemberInfo memberInfo) } } - /// - /// This delegate describes the signature of a function that can be used - /// to convert a list of strings into a single enum value. - /// - /// - /// The type of enum to convert the list of strings to a value of. - /// - /// - /// Strings to convert into an enum value. - /// - /// - /// The enum value that results from performing a bitwise OR operation - /// on the list of enum values that result from converting each item in - /// the provided list of strings to an enum value of the indicated type. - /// - /// - /// True if the parsing of flags was successful, false otherwise. - /// - protected delegate bool TryParseEnumDelegate(IList - stringFlags, out TEnum result) where TEnum : struct, Enum; - - /// - /// Private static wrapper to handle converting a list of strings into - /// a single enum value. - /// - /// - /// The type of enum to convert the list of strings into. - /// - /// - /// The list of strings to convert into a single enum value. - /// - /// - /// The function used to convert the list of strings into a single enum - /// value. - /// - /// A single enum value that is the result of a bitwise OR - /// operation between the enum values that result from parsing each of - /// the items in a list into the indicated enum type value. - /// - protected static TEnum StringsToEnum( - IList stringFlags, - TryParseEnumDelegate parseEnumFn) - where TEnum : struct, Enum - { - _ = parseEnumFn(stringFlags, out TEnum result); - return result; - } - /// /// Gets an IEnumerable of the type / value pairs for each Field or /// Property matching the given BindingFlags on the given instance. diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index 19d65362c..6ac0f5b08 100644 --- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs +++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs @@ -477,7 +477,7 @@ private PlatformInterface CreatePlatformInterface() #if UNITY_EDITOR PlatformFlags.LoadingInEditor; #else - configData.platformOptionsFlags.Unwrap(); + configData.GetPlatformFlags(); #endif if (configData.IsEncryptionKeyValid()) { diff --git a/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs b/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs index d3b0a659f..6ec9ccea6 100644 --- a/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs +++ b/com.playeveryware.eos/Runtime/Core/Utility/FileSystemUtility.cs @@ -713,9 +713,9 @@ private static bool ExistsInternal(string path, bool isDirectory = false) { exists = File.Exists(path); } -#endif return exists; +#endif } public static async Task DirectoryExistsAsync(string path) From 574244505f2ad44d2cd447ca7f9a454857e6f440 Mon Sep 17 00:00:00 2001 From: arthur740212 Date: Thu, 24 Oct 2024 18:22:54 -0700 Subject: [PATCH 4/4] fix(log,init) : llc missing fix --- .../Runtime/Core/Utility/LogLevelUtility.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/com.playeveryware.eos/Runtime/Core/Utility/LogLevelUtility.cs b/com.playeveryware.eos/Runtime/Core/Utility/LogLevelUtility.cs index 59a9a0afe..92f51122c 100644 --- a/com.playeveryware.eos/Runtime/Core/Utility/LogLevelUtility.cs +++ b/com.playeveryware.eos/Runtime/Core/Utility/LogLevelUtility.cs @@ -61,6 +61,11 @@ public static List LogLevelList return null; } + if (logLevelConfig.LogCategoryLevelPairs == null) + { + return null; + } + List logLevels = new List(); for (int i = 0; i < LogCategoryStringArray.Length - 1; i++) {