// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Standard Library using System; using System.IO; using System.Text; // Unity using UnityEditor; using UnityEngine; using UnityEngine.Networking; // GameKit using AWS.GameKit.Common; using AWS.GameKit.Common.Models; using AWS.GameKit.Runtime.Utils; using AWS.GameKit.Runtime.Models; namespace AWS.GameKit.Runtime.Core { /// /// This class provides a high level interface for the SessionManager. /// Call SessionManager.Get() to get the singleton instance of this class. /// /// /// The SessionManager provides APIs for loading and querying the "awsGameKitClientConfig.yaml" file, also known as "the config file".

/// /// The config file is loaded by calling ReloadConfig() or CopyAndReloadConfig().

/// /// The config file contains settings that are needed at runtime in order to call any of the GameKit feature APIs. /// The config file's settings are specific to the currently selected environment code (i.e. "dev", "qa", "prd", etc.). /// The environment can be changed in the "Environment & Credentials" page of the AWS GameKit Settings Window.

/// /// The config file is automatically created/updated each time a GameKit feature is created or redeployed, or when the environment changes (i.e. "dev", "qa", "prd", etc.).

/// /// The config file is located in the folder "Assets/AWS GameKit/Resources/" and is automatically included in builds of your game. /// It is automatically included because it resides in a folder with the special name "Resources". /// Learn more about Unity's special "Resources/" folder name at: https://docs.unity3d.com/Manual/SpecialFolders.html ///
public class SessionManager : Singleton { private readonly SessionManagerWrapper _wrapper = SessionManagerWrapper.Get(); private class ConfigPath { public const string NAME_WITHOUT_EXTENSION = "awsGameKitClientConfig"; /// /// The filename of the client config file located in the special "Resources/" folder. /// public const string DESTINATION_NAME = NAME_WITHOUT_EXTENSION + ".yaml"; #if UNITY_EDITOR /// /// The filename of the client config files located in the environment specific "InstanceFiles/" folders. /// /// /// The source and destination files have different extensions (.yml and .yaml respectively). /// The .yaml extension is required in order to load the file as a TextAsset with Resources.Load() (see https://docs.unity3d.com/Manual/class-TextAsset.html). /// The .yml extension is generated by the underlying AWS GameKit C++ Library. /// The file extension is changed when the file is copied to the destination location by CopyAndReloadConfig(). /// private const string SOURCE_NAME = NAME_WITHOUT_EXTENSION + ".yml"; private string SourceRelativePathFromPackageFolder => Path.Combine("Editor", "CloudResources", "InstanceFiles", _gameAlias, _environmentCode, SOURCE_NAME); internal string SourceAbsolutePath => Path.Combine(GameKitPaths.Get().ASSETS_FULL_PATH, SourceRelativePathFromPackageFolder); internal string SourceAssetDatabasePath => Path.Combine(GameKitPaths.Get().ASSETS_RELATIVE_PATH, SourceRelativePathFromPackageFolder); internal string DestinationAssetDatabasePath => Path.Combine(GameKitPaths.Get().ASSETS_RESOURCES_RELATIVE_PATH, DESTINATION_NAME); private readonly string _gameAlias; private readonly string _environmentCode; public ConfigPath(string gameAlias, string environmentCode) { _gameAlias = gameAlias; _environmentCode = environmentCode; } #endif } /// /// Set a specified token's value. /// /// The type of token to set. /// The value of the token public void SetToken(TokenType tokenType, string value) { _wrapper.SessionManagerSetToken(tokenType, value); } /// /// Check if settings are loaded for the passed in feature. /// /// /// These settings are found in the config file, which is described in the class documentation. /// The file is loaded by calling either ReloadConfig() or CopyAndReloadConfig(). /// /// The feature to check. /// True if the feature's settings are loaded, false otherwise. public bool AreSettingsLoaded(FeatureType featureType) { return _wrapper.SessionManagerAreSettingsLoaded(featureType); } /// /// Reload the config file from the special "Resources/" folder. /// /// /// The config file is described in the class documentation. /// public void ReloadConfig() { Logging.LogInfo("SessionManagerWrapper::ReloadConfig()"); TextAsset configFile = Resources.Load(ConfigPath.NAME_WITHOUT_EXTENSION); if (configFile == null) { if (!Application.isPlaying) { // Don't log anything during Edit Mode. // The config file won't exist until the user has submitted their AWS Credentials. return; } string howToFix = Application.isEditor ? "re-enter Play Mode" : "re-build the game"; Logging.LogError($"The {ConfigPath.DESTINATION_NAME} file is missing. AWS GameKit feature APIs will not work. " + $"Please make sure at least one AWS GameKit feature is deployed and then {howToFix}."); return; } _wrapper.SessionManagerReloadConfigContents(configFile.text); } /// /// Inject the CA certificate into the client config and load it. /// /// /// Use this for Mobile devices that need a CA Cert for the CURL Http client. /// internal void ReloadConfigMobile() { Logging.LogInfo("SessionManagerWrapper::ReloadConfigMobile()"); #if UNITY_ANDROID // Helper to build URI to asset inside a jar/apk Func buildJarAssetPath = (string rawAsset) => { return $"jar:file://{Application.dataPath}!/assets/raw/{rawAsset}"; }; // Helper to read an asset from a jar/apk. This blocks so only use it to read small assets. Func readTextFromJarAsset = (string fullPath) => { Logging.LogInfo($"Reading asset from jar {fullPath}"); UnityWebRequest request = UnityWebRequest.Get(fullPath); request.SendWebRequest(); while (!request.isDone) { // NO-OP } if (request.result != UnityWebRequest.Result.Success) { Logging.LogError($"Could not read asset {fullPath}, error {request.error}"); return string.Empty; } return request.downloadHandler.text; }; // Load client config text string clientConfigPath = buildJarAssetPath(ConfigPath.DESTINATION_NAME); string clientConfigContent = readTextFromJarAsset(clientConfigPath); // Load CA Certificate and save it to Persistent Data Path if it doesn't exist string caCertReadPath = buildJarAssetPath("cacert.pem"); string caCertWritePath = $"{Application.persistentDataPath}/cacert.pem"; // persistentDataPath ends in / if (!File.Exists(caCertWritePath)) { Logging.LogInfo($"Writing CA Certificate to file {caCertWritePath}"); string caCertContent = readTextFromJarAsset(caCertReadPath); File.WriteAllText(caCertWritePath, caCertContent); } else { Logging.LogInfo($"CA Certificate found at {caCertWritePath}"); } // Add CA Certificate to client config. We need this for HTTPS calls to the backend Logging.LogInfo("Updating Client config"); StringBuilder sb = new StringBuilder(clientConfigContent); sb.Append(System.Environment.NewLine); sb.Append($"ca_cert_file: {caCertWritePath}"); sb.Append(System.Environment.NewLine); Logging.LogInfo("Reloading Client config"); _wrapper.SessionManagerReloadConfigContents(sb.ToString()); #endif #if UNITY_IPHONE TextAsset configFile = Resources.Load(ConfigPath.NAME_WITHOUT_EXTENSION); string sslPemDataPath = $"{Application.dataPath}/Security/Certs/cacert.pem"; StringBuilder sb = new StringBuilder(configFile.text); sb.Append(System.Environment.NewLine); sb.Append($"ca_cert_file: {sslPemDataPath}"); sb.Append(System.Environment.NewLine); _wrapper.SessionManagerReloadConfigContents(sb.ToString()); #endif } /// /// Releases all resources for the SessionManager. /// /// /// Should only be called by the GameKitManager. Calling outside of the manager many cause runtime exceptions. public virtual void Release() { _wrapper.ReleaseInstance(); } #if UNITY_EDITOR /// /// Copy the specific environment's config file to the special "Resources/" folder, then reload settings from this config file. /// /// /// This method is only called in Editor Mode. It is called whenever a GameKit feature is created or redeployed, or when the environment changes (i.e. "dev", "qa", "prd", etc.).

/// /// The purpose of this method is to persist the current environment's config file to the special "Resources/" folder so it can be loaded by ReloadConfig().

/// /// The config file and the special "Resources/" folder are described in the class documentation. ///
/// The game's alias, ex: "mygame". /// The environment to copy the config from, ex: "dev". public void CopyAndReloadConfig(string gameAlias, string environmentCode) { Debug.Log($"SessionManagerWrapper::CopyAndReloadConfig({nameof(gameAlias)}={gameAlias}, {nameof(environmentCode)}={environmentCode})"); ConfigPath configPath = new ConfigPath(gameAlias, environmentCode); if (!File.Exists(configPath.SourceAbsolutePath)) { Logging.LogError($"Source config file does not exist: {configPath.SourceAbsolutePath}"); return; } try { AssetDatabase.ImportAsset(configPath.SourceAssetDatabasePath, ImportAssetOptions.ForceSynchronousImport); AssetDatabase.CopyAsset(configPath.SourceAssetDatabasePath, configPath.DestinationAssetDatabasePath); Logging.LogInfo($"Copied config file from {configPath.SourceAssetDatabasePath} to {configPath.DestinationAssetDatabasePath}"); } catch (Exception e) { Logging.LogException("Error copying config", e); ClearSettings(); return; } ReloadConfig(); } /// /// Copy the specific environment's config file to the special Mobile package folder that gets deployed to a device. /// /// The game's alias, ex: "mygame". /// The environment to copy the config from, ex: "dev". internal void CopyConfigToMobileAssets(string gameAlias, string environmentCode) { Debug.Log($"SessionManagerWrapper::CopyConfigToMobileAssets({nameof(gameAlias)}={gameAlias}, {nameof(environmentCode)}={environmentCode})"); ConfigPath configPath = new ConfigPath(gameAlias, environmentCode); if (!File.Exists(configPath.SourceAbsolutePath)) { Logging.LogError($"Source config file does not exist: {configPath.SourceAbsolutePath}"); return; } #if UNITY_ANDROID try { string androidLibDirFullPath = Path.Combine(GameKitPaths.Get().ASSETS_FULL_PATH, "Plugins", "Android", "GameKitConfig.androidlib").Replace("\\", "/"); string androidLibRawAssetDirFullPath = Path.Combine(androidLibDirFullPath, "assets", "raw").Replace("\\", "/"); string androidLibDirRelativePath = Path.Combine(GameKitPaths.Get().ASSETS_RELATIVE_PATH, "Plugins", "Android", "GameKitConfig.androidlib").Replace("\\", "/"); string androidLibRawAssetDirRelativePath = Path.Combine(androidLibDirRelativePath, "assets", "raw").Replace("\\", "/"); string androidLibRawAssetConfigPath = Path.Combine(androidLibRawAssetDirRelativePath, ConfigPath.DESTINATION_NAME).Replace("\\", "/"); AssetDatabase.CopyAsset(configPath.DestinationAssetDatabasePath, androidLibRawAssetConfigPath); Logging.LogInfo($"Copied Android config file from {configPath.SourceAssetDatabasePath} to {androidLibRawAssetConfigPath}"); } catch (Exception e) { Logging.LogException("Error copying Mobile config", e); ClearSettings(); return; } #endif } /// /// Return true if a config file exists for the environment, false otherwise. /// /// The game's alias, ex: "mygame". /// The environment to copy the config from, ex: "dev". public bool DoesConfigFileExist(string gameAlias, string environmentCode) { ConfigPath configPath = new ConfigPath(gameAlias, environmentCode); return File.Exists(configPath.SourceAbsolutePath); } /// /// Clear out the currently loaded client config settings. /// private void ClearSettings() { _wrapper.SessionManagerReloadConfigFile(string.Empty); } #endif } }