initial upload
This commit is contained in:
3
Assets/Convai/Scripts/Runtime/PlayerStats/API.meta
Normal file
3
Assets/Convai/Scripts/Runtime/PlayerStats/API.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a9c1b68eb2a4b2c8aced3e1739f7f1e
|
||||
timeCreated: 1720516527
|
||||
@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Assets.Convai.Scripts.Runtime.PlayerStats.API.Model;
|
||||
using Convai.Scripts.Runtime.LoggerSystem;
|
||||
using Convai.Scripts.Runtime.PlayerStats.API.Model;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats.API {
|
||||
public static class LongTermMemoryAPI {
|
||||
private const string BETA_SUBDOMAIN = "beta";
|
||||
private const string PROD_SUBDOMAIN = "api";
|
||||
private const string BASE_URL = "https://{0}.convai.com/";
|
||||
|
||||
private const bool IS_ON_PROD = false;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request to the Convai API to create a new speaker ID
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API Key of the user</param>
|
||||
/// <param name="playerName">Player Name which will be used to create Speaker ID</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<string> CreateNewSpeakerID ( string apiKey, string playerName, Action onFail = null ) {
|
||||
if ( string.IsNullOrEmpty( apiKey ) )
|
||||
return string.Empty;
|
||||
HttpClient client = CreateHttpClient( apiKey );
|
||||
HttpContent content = CreateHttpContent( new Dictionary<string, object>
|
||||
{
|
||||
{ "name", playerName }
|
||||
} );
|
||||
string endPoint = GetEndPoint( NEW_SPEAKER );
|
||||
string response = await SendPostRequestAsync( endPoint, client, content );
|
||||
return ExtractSpeakerIDFromResponse( response, onFail );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request to the Convai API to update the status of Long Term Memory for that character
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API Key of the user</param>
|
||||
/// <param name="charID">Character ID of the Convai NPC</param>
|
||||
/// <param name="isEnabled">Status of LTM</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> ToggleLTM ( string apiKey, string charID, bool isEnabled, Action onFail = null ) {
|
||||
if ( string.IsNullOrEmpty( apiKey ) )
|
||||
return false;
|
||||
HttpClient client = CreateHttpClient( apiKey );
|
||||
CharacterUpdateRequest request = new( charID, isEnabled );
|
||||
string serializeObject = JsonConvert.SerializeObject( request );
|
||||
HttpContent content = CreateHttpContent( serializeObject );
|
||||
string endPoint = GetEndPoint( CHARACTER_UPDATE );
|
||||
string response = await SendPostRequestAsync( endPoint, client, content );
|
||||
return ExtractToggleLTMResult( response, onFail );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request to get the status of Long Term Memory for that character
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API Key of the user</param>
|
||||
/// <param name="charID">Character ID of the Convai NPC</param>
|
||||
/// <param name="onFail">Action which will be invoked when web requests fails</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> GetLTMStatus ( string apiKey, string charID, Action onFail = null ) {
|
||||
if ( string.IsNullOrEmpty( apiKey ) )
|
||||
return false;
|
||||
HttpClient client = CreateHttpClient( apiKey );
|
||||
HttpContent content = CreateHttpContent( new Dictionary<string, object>
|
||||
{
|
||||
{ "charID", charID }
|
||||
} );
|
||||
string endPoint = GetEndPoint( CHARACTER_GET );
|
||||
string response = await SendPostRequestAsync( endPoint, client, content );
|
||||
return ExtractLTMStatusResult( response, onFail );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets List of Speaker ID(s) associated with the given API Key
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API Key of the Invoker</param>
|
||||
/// <param name="onFail">Action to be invoked in case of failure or exception</param>
|
||||
/// <returns>List of Speaker ID Details</returns>
|
||||
public static async Task<List<SpeakerIDDetails>> GetSpeakerIDList ( string apiKey, Action onFail = null ) {
|
||||
if ( string.IsNullOrEmpty( apiKey ) )
|
||||
return null;
|
||||
HttpClient client = CreateHttpClient( apiKey );
|
||||
HttpContent content = CreateHttpContent( string.Empty );
|
||||
string endPoint = GetEndPoint( SPEAKER_ID_LIST );
|
||||
string response = await SendPostRequestAsync( endPoint, client, content );
|
||||
return ExtractSpeakerIDList( response, onFail );
|
||||
}
|
||||
/// <summary>
|
||||
/// Delete the Speaker ID for the Given API Key
|
||||
/// </summary>
|
||||
/// <param name="apiKey">API Key of the Invoker</param>
|
||||
/// <param name="speakerID">Speaker ID to be Deleted</param>
|
||||
/// <param name="onFail">Action to be invoked in case of failure or exception</param>
|
||||
/// <returns>True, if ID is deleted successfully, otherwise false</returns>
|
||||
public static async Task<bool> DeleteSpeakerID(string apiKey, string speakerID, Action onFail = null ) {
|
||||
if ( string.IsNullOrEmpty( apiKey ) )
|
||||
return false;
|
||||
HttpClient client = CreateHttpClient( apiKey );
|
||||
HttpContent content = CreateHttpContent( new Dictionary<string, object> {
|
||||
{"speakerId", speakerID }
|
||||
} );
|
||||
string endPoint = GetEndPoint( DELETE_SPEAKER_ID );
|
||||
string response = await SendPostRequestAsync( endPoint, client, content );
|
||||
if(string.IsNullOrEmpty( response ) ) {
|
||||
onFail?.Invoke();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Extraction
|
||||
private static bool ExtractLTMStatusResult ( string response, Action onFail = null ) {
|
||||
try {
|
||||
CharacterGetResponse characterGetResponse = JsonConvert.DeserializeObject<CharacterGetResponse>( response );
|
||||
return characterGetResponse.MemorySettings.IsEnabled;
|
||||
}
|
||||
catch ( Exception exception ) {
|
||||
ConvaiLogger.Error( $"Exception caught: {exception.Message}", ConvaiLogger.LogCategory.GRPC );
|
||||
onFail?.Invoke();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ExtractToggleLTMResult ( string response, Action onFail = null ) {
|
||||
try {
|
||||
ServerRequestResponse serverRequestResponse = JsonConvert.DeserializeObject<ServerRequestResponse>( response );
|
||||
return serverRequestResponse.Status == "SUCCESS";
|
||||
}
|
||||
catch ( Exception exception ) {
|
||||
ConvaiLogger.Exception( $"Exception caught: {exception.Message}", ConvaiLogger.LogCategory.GRPC );
|
||||
onFail?.Invoke();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractSpeakerIDFromResponse ( string response, Action onFail = null ) {
|
||||
try {
|
||||
ServerRequestResponse serverRequestResponse = JsonConvert.DeserializeObject<ServerRequestResponse>( response );
|
||||
return serverRequestResponse.SpeakerID;
|
||||
}
|
||||
catch ( Exception exception ) {
|
||||
ConvaiLogger.Exception( $"Exception caught: {exception.Message}", ConvaiLogger.LogCategory.GRPC );
|
||||
onFail?.Invoke();
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SpeakerIDDetails> ExtractSpeakerIDList ( string response, Action onFail = null ) {
|
||||
try {
|
||||
List<SpeakerIDDetails> speakers = JsonConvert.DeserializeObject<List<SpeakerIDDetails>>( response );
|
||||
return speakers;
|
||||
}
|
||||
catch ( Exception exception ) {
|
||||
ConvaiLogger.Exception( $"Exception caught: {exception.Message}", ConvaiLogger.LogCategory.GRPC );
|
||||
onFail?.Invoke();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region HTTP Request Creation
|
||||
private static string GetEndPoint ( string api ) {
|
||||
return string.Format( BASE_URL, IS_ON_PROD ? PROD_SUBDOMAIN : BETA_SUBDOMAIN ) + api;
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient ( string apiKey ) {
|
||||
if ( string.IsNullOrEmpty( apiKey ) )
|
||||
return new HttpClient();
|
||||
HttpClient httpClient = new() {
|
||||
Timeout = TimeSpan.FromSeconds( 30 ),
|
||||
DefaultRequestHeaders =
|
||||
{
|
||||
Accept =
|
||||
{
|
||||
new MediaTypeWithQualityHeaderValue("application/json")
|
||||
}
|
||||
}
|
||||
};
|
||||
httpClient.DefaultRequestHeaders.Add( "CONVAI-API-KEY", apiKey );
|
||||
return httpClient;
|
||||
}
|
||||
private static HttpContent CreateHttpContent ( Dictionary<string, object> dataToSend ) {
|
||||
// Serialize the dictionary to JSON
|
||||
string json = JsonConvert.SerializeObject( dataToSend );
|
||||
|
||||
// Convert JSON to HttpContent
|
||||
return new StringContent( json, Encoding.UTF8, "application/json" );
|
||||
}
|
||||
|
||||
private static HttpContent CreateHttpContent ( string json ) {
|
||||
// Convert JSON to HttpContent
|
||||
return new StringContent( json, Encoding.UTF8, "application/json" );
|
||||
}
|
||||
|
||||
private static async Task<string> SendPostRequestAsync ( string endpoint, HttpClient httpClient, HttpContent content ) {
|
||||
try {
|
||||
HttpResponseMessage response = await httpClient.PostAsync( endpoint, content );
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch ( HttpRequestException e ) {
|
||||
ConvaiLogger.Error( $"Request to {endpoint} failed: {e.Message}", ConvaiLogger.LogCategory.GRPC );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region END POINTS
|
||||
|
||||
private const string NEW_SPEAKER = "user/speaker/new";
|
||||
private const string SPEAKER_ID_LIST = "user/speaker/list";
|
||||
private const string DELETE_SPEAKER_ID = "user/speaker/delete";
|
||||
private const string CHARACTER_UPDATE = "character/update";
|
||||
private const string CHARACTER_GET = "character/get";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 431a601977264c8ca9a7c365826a7702
|
||||
timeCreated: 1720511365
|
||||
3
Assets/Convai/Scripts/Runtime/PlayerStats/API/Model.meta
Normal file
3
Assets/Convai/Scripts/Runtime/PlayerStats/API/Model.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54c977d1a702424287be90dc66636b59
|
||||
timeCreated: 1720516631
|
||||
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats.API.Model
|
||||
{
|
||||
public class CharacterGetResponse
|
||||
{
|
||||
[JsonProperty("memory_settings")] public MemorySettings MemorySettings { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bb05f1a3e15401a84a561a9257d9945
|
||||
timeCreated: 1720606728
|
||||
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats.API.Model
|
||||
{
|
||||
[Serializable]
|
||||
public class CharacterUpdateRequest
|
||||
{
|
||||
public CharacterUpdateRequest(string characterID, bool isEnabled)
|
||||
{
|
||||
CharacterID = characterID;
|
||||
MemorySettings = new MemorySettings(isEnabled);
|
||||
}
|
||||
|
||||
[JsonProperty("charID")] public string CharacterID { get; set; }
|
||||
[JsonProperty("memorySettings")] public MemorySettings MemorySettings { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af9b63038bda496c987d8b926d89ad6e
|
||||
timeCreated: 1720533023
|
||||
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats.API.Model
|
||||
{
|
||||
[Serializable]
|
||||
public class MemorySettings
|
||||
{
|
||||
public MemorySettings(bool isEnabled)
|
||||
{
|
||||
IsEnabled = isEnabled;
|
||||
}
|
||||
|
||||
[JsonProperty("enabled")] public bool IsEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4983b6179b64409a76bb7752c84d993
|
||||
timeCreated: 1720606650
|
||||
@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats.API.Model
|
||||
{
|
||||
public class ServerRequestResponse
|
||||
{
|
||||
[JsonProperty("STATUS")] public string Status { get; private set; }
|
||||
|
||||
[JsonProperty("speaker_id")] public string SpeakerID { get; private set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13df68343bcd48c2a34dcbfa845e95b4
|
||||
timeCreated: 1720516664
|
||||
@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Assets.Convai.Scripts.Runtime.PlayerStats.API.Model {
|
||||
public class SpeakerIDDetails {
|
||||
[JsonProperty("speaker_id")] public string ID { get; set; }
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d22480612d861e847b4fcebf608c295f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Runtime.LoggerSystem;
|
||||
using Convai.Scripts.Runtime.PlayerStats.API;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats {
|
||||
public class ConvaiPlayerDataHandler : MonoBehaviour {
|
||||
public const string PLAYER_NAME_SAVE_KEY = "PlayerName";
|
||||
public const string SPEAKER_ID_SAVE_KEY = "PlayerSpeakerID";
|
||||
[field: SerializeField] public ConvaiPlayerDataSO ConvaiPlayerDataSO { get; private set; }
|
||||
|
||||
private void Awake () {
|
||||
InitializeDataContainer();
|
||||
}
|
||||
|
||||
private async void Start () {
|
||||
LoadData();
|
||||
|
||||
if ( !ConvaiPlayerDataSO.CreateSpeakerIDIfNotFound )
|
||||
return;
|
||||
if ( !string.IsNullOrEmpty( ConvaiPlayerDataSO.SpeakerID ) )
|
||||
return;
|
||||
await SetNewSpeakerID();
|
||||
}
|
||||
|
||||
private void OnDestroy () {
|
||||
SaveData();
|
||||
}
|
||||
|
||||
private async Task SetNewSpeakerID () {
|
||||
string speakerID = await CreateSpeakerID( ConvaiPlayerDataSO.PlayerName, true, true,
|
||||
() => { ConvaiLogger.DebugLog( "Could not create a new speaker ID, please try again", ConvaiLogger.LogCategory.Editor ); } );
|
||||
if ( !string.IsNullOrEmpty( speakerID ) )
|
||||
ConvaiPlayerDataSO.SpeakerID = speakerID;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the Player data from Player Prefs
|
||||
/// </summary>
|
||||
public void LoadData () {
|
||||
if ( ConvaiPlayerDataSO == null )
|
||||
return;
|
||||
ConvaiPlayerDataSO.PlayerName =
|
||||
PlayerPrefs.GetString( PLAYER_NAME_SAVE_KEY, ConvaiPlayerDataSO.DefaultPlayerName );
|
||||
ConvaiPlayerDataSO.SpeakerID = PlayerPrefs.GetString( SPEAKER_ID_SAVE_KEY, string.Empty );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the Player data to Player Prefs
|
||||
/// </summary>
|
||||
public void SaveData () {
|
||||
if ( ConvaiPlayerDataSO == null )
|
||||
return;
|
||||
PlayerPrefs.SetString( PLAYER_NAME_SAVE_KEY, ConvaiPlayerDataSO.PlayerName );
|
||||
PlayerPrefs.SetString( SPEAKER_ID_SAVE_KEY, ConvaiPlayerDataSO.SpeakerID );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request to Convai API to create a new speaker ID
|
||||
/// </summary>
|
||||
/// <param name="playerName">Player name which will be used to create new Speaker ID</param>
|
||||
/// <param name="randomPrefix">Decides if this function will add a random prefix to make the player name unique</param>
|
||||
/// <param name="randomSuffix">Decides if this function will add a random suffix to make the player name unique</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> CreateSpeakerID ( string playerName, bool randomPrefix = false, bool randomSuffix = false, Action onFail = null ) {
|
||||
if ( !ConvaiAPIKeySetup.GetAPIKey( out string apiKey ) )
|
||||
return string.Empty;
|
||||
string response = await LongTermMemoryAPI.CreateNewSpeakerID( apiKey, playerName, onFail );
|
||||
ConvaiLogger.DebugLog( $"Created SpeakerID = {response ?? "Unsuccessful"} for Player Name = {playerName}", ConvaiLogger.LogCategory.Character );
|
||||
return response;
|
||||
}
|
||||
|
||||
#region Class Utility
|
||||
|
||||
private string CreateRandomString ( int length ) {
|
||||
return Guid.NewGuid().ToString().Substring( 0, length );
|
||||
}
|
||||
|
||||
private void InitializeDataContainer () {
|
||||
if ( ConvaiPlayerDataSO != null )
|
||||
return;
|
||||
if ( !ConvaiPlayerDataSO.GetPlayerData( out ConvaiPlayerDataSO dataSO ) )
|
||||
return;
|
||||
ConvaiPlayerDataSO = dataSO;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10acdfbf72306a443bcda23840b87694
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,56 @@
|
||||
using System.IO;
|
||||
using Convai.Scripts.Runtime.Attributes;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Convai.Scripts.Runtime.PlayerStats {
|
||||
[CreateAssetMenu( menuName = "Convai/Player Data", fileName = nameof( ConvaiPlayerDataSO ) )]
|
||||
public class ConvaiPlayerDataSO : ScriptableObject {
|
||||
[field: SerializeField] public string DefaultPlayerName { get; private set; } = "Player";
|
||||
[field: SerializeField] public string PlayerName { get; set; }
|
||||
|
||||
[field: ReadOnly]
|
||||
[field: SerializeField]
|
||||
public string SpeakerID { get; set; }
|
||||
|
||||
[field: SerializeField] public bool CreateSpeakerIDIfNotFound { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the PlayerDataSO if found in the Resources folder
|
||||
/// </summary>
|
||||
/// <param name="playerDataSO">Reference of the Player Data</param>
|
||||
/// <returns>Returns true if found otherwise false</returns>
|
||||
public static bool GetPlayerData ( out ConvaiPlayerDataSO playerDataSO ) {
|
||||
playerDataSO = Resources.Load<ConvaiPlayerDataSO>( nameof( ConvaiPlayerDataSO ) );
|
||||
#if UNITY_EDITOR
|
||||
if ( playerDataSO == null ) {
|
||||
playerDataSO = CreateInstance<ConvaiPlayerDataSO>();
|
||||
CreatePlayerDataSO( playerDataSO );
|
||||
}
|
||||
#endif
|
||||
return playerDataSO != null;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static void CreatePlayerDataSO ( ConvaiPlayerDataSO convaiPlayerData ) {
|
||||
string assetPath = "Assets/Convai/Resources/ConvaiPlayerDataSO.asset";
|
||||
|
||||
if ( !File.Exists( assetPath ) ) {
|
||||
if ( !AssetDatabase.IsValidFolder( "Assets/Convai/Resources" ) )
|
||||
AssetDatabase.CreateFolder( "Assets/Convai", "Resources" );
|
||||
|
||||
AssetDatabase.CreateAsset( convaiPlayerData, assetPath );
|
||||
}
|
||||
else {
|
||||
AssetDatabase.DeleteAsset( assetPath );
|
||||
AssetDatabase.CreateAsset( convaiPlayerData, assetPath );
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47deca9d54168f944b352427b36d0ac6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user