// 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
}
}