Initial Commit
This commit is contained in:
+293
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Meta.WitAi.Configuration;
|
||||
using Meta.WitAi.Data.Info;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Meta.WitAi.Data.Configuration
|
||||
{
|
||||
public class WitConfiguration : ScriptableObject, IWitRequestConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Access token used in builds to make requests for data from Wit.ai
|
||||
/// </summary>
|
||||
[Tooltip("Access token used in builds to make requests for data from Wit.ai")]
|
||||
[FormerlySerializedAs("clientAccessToken")]
|
||||
[SerializeField] private string _clientAccessToken;
|
||||
|
||||
/// <summary>
|
||||
/// Application info
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("application")]
|
||||
[SerializeField] private WitAppInfo _appInfo; //to be replaced by _configData
|
||||
|
||||
/// <summary>
|
||||
/// Configuration data about the app.
|
||||
/// </summary>
|
||||
[SerializeField] private WitConfigurationAssetData[] _configData;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration id
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("configId")]
|
||||
[HideInInspector] [SerializeField] private string _configurationId;
|
||||
|
||||
[Tooltip("The number of milliseconds to wait before requests to Wit.ai will timeout")]
|
||||
[SerializeField] public int timeoutMS = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration parameters to set up a custom endpoint for testing purposes and request forwarding. The default values here will work for most.
|
||||
/// </summary>
|
||||
[Tooltip("Configuration parameters to set up a custom endpoint for testing purposes and request forwarding. The default values here will work for most.")]
|
||||
[SerializeField] public WitEndpointConfig endpointConfiguration = new WitEndpointConfig();
|
||||
|
||||
/// <summary>
|
||||
/// True if this configuration should not show up in the demo list
|
||||
/// </summary>
|
||||
[SerializeField] public bool isDemoOnly;
|
||||
|
||||
/// <summary>
|
||||
/// When set to true, will use Conduit to dispatch voice commands.
|
||||
/// </summary>
|
||||
[Tooltip("Conduit enables manifest-based dispatching to invoke callbacks with native types directly without requiring manual parsing.")]
|
||||
[SerializeField] public bool useConduit = true;
|
||||
|
||||
/// <summary>
|
||||
/// The path to the Conduit manifest.
|
||||
/// </summary>
|
||||
[SerializeField] private string _manifestLocalPath;
|
||||
|
||||
/// <summary>
|
||||
/// The assemblies that we want to exclude from Conduit.
|
||||
/// </summary>
|
||||
[SerializeField] public List<string> excludedAssemblies = new List<string>
|
||||
{
|
||||
"Oculus.Voice.Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||
"Meta.WitAi.Samples, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
};
|
||||
|
||||
[Tooltip("When true, Conduit will attempt to match incoming requests by type when no exact matches are found. This increases tolerance but reduces runtime performance.")]
|
||||
[SerializeField] public bool relaxedResolution;
|
||||
|
||||
/// <summary>
|
||||
/// Safe access of local path
|
||||
/// </summary>
|
||||
public string ManifestLocalPath
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (string.IsNullOrEmpty(_manifestLocalPath))
|
||||
{
|
||||
_manifestLocalPath = $"ConduitManifest-{Guid.NewGuid()}.json";
|
||||
SaveConfiguration();
|
||||
}
|
||||
#endif
|
||||
return _manifestLocalPath;
|
||||
}
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Returns manifest full editor path
|
||||
/// </summary>
|
||||
public string GetManifestEditorPath()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_manifestLocalPath)) return string.Empty;
|
||||
|
||||
string lookup = Path.GetFileNameWithoutExtension(_manifestLocalPath);
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets(lookup);
|
||||
if (guids != null && guids.Length > 0)
|
||||
{
|
||||
return UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Reset all data
|
||||
/// </summary>
|
||||
public void ResetData()
|
||||
{
|
||||
_configurationId = null;
|
||||
_appInfo = new WitAppInfo();
|
||||
endpointConfiguration = new WitEndpointConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the individual data components of the configuration.
|
||||
/// </summary>
|
||||
public void UpdateDataAssets()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
RefreshPlugins();
|
||||
#endif
|
||||
|
||||
foreach (WitConfigurationAssetData data in _configData)
|
||||
{
|
||||
data.Refresh(this);
|
||||
}
|
||||
}
|
||||
// Logger invalid warnings
|
||||
private const string INVALID_APP_ID_NO_CLIENT_TOKEN = "App Info Not Set - No Client Token";
|
||||
private const string INVALID_APP_ID_WITH_CLIENT_TOKEN =
|
||||
"App Info Not Set - Has Client Token";
|
||||
public string GetLoggerAppId()
|
||||
{
|
||||
// Get application id
|
||||
string applicationId = GetApplicationId();
|
||||
if (String.IsNullOrEmpty(applicationId))
|
||||
{
|
||||
// NOTE: If a dev only provides a client token we may not have the application id.
|
||||
string clientAccessToken = GetClientAccessToken();
|
||||
if (!string.IsNullOrEmpty(clientAccessToken))
|
||||
{
|
||||
return INVALID_APP_ID_WITH_CLIENT_TOKEN;
|
||||
}
|
||||
return INVALID_APP_ID_NO_CLIENT_TOKEN;
|
||||
}
|
||||
return applicationId;
|
||||
}
|
||||
|
||||
#region IWitRequestConfiguration
|
||||
/// <summary>
|
||||
/// Returns unique configuration guid
|
||||
/// </summary>
|
||||
public string GetConfigurationId()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// Ensure configuration id is generated
|
||||
if (string.IsNullOrEmpty(_configurationId))
|
||||
{
|
||||
_configurationId = Guid.NewGuid().ToString();
|
||||
}
|
||||
#endif
|
||||
// Return configuration id
|
||||
return _configurationId;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns unique application id
|
||||
/// </summary>
|
||||
public string GetApplicationId() => _appInfo.id;
|
||||
/// <summary>
|
||||
/// Returns application info
|
||||
/// </summary>
|
||||
public WitAppInfo GetApplicationInfo() => _appInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the configuration data for this app.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public WitConfigurationAssetData[] GetConfigData()
|
||||
{
|
||||
if (_configData == null)
|
||||
{
|
||||
_configData = Array.Empty<WitConfigurationAssetData>();
|
||||
}
|
||||
return _configData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return endpoint override
|
||||
/// </summary>
|
||||
public IWitRequestEndpointInfo GetEndpointInfo() => endpointConfiguration;
|
||||
/// <summary>
|
||||
/// Returns client access token
|
||||
/// </summary>
|
||||
public string GetClientAccessToken()
|
||||
{
|
||||
return _clientAccessToken;
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Editor only setter
|
||||
/// </summary>
|
||||
public void SetClientAccessToken(string newToken)
|
||||
{
|
||||
_clientAccessToken = newToken;
|
||||
SaveConfiguration();
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns server access token (Editor Only)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public string GetServerAccessToken()
|
||||
{
|
||||
return WitAuthUtility.GetAppServerToken(GetApplicationId());
|
||||
}
|
||||
/// <summary>
|
||||
/// Set application info
|
||||
/// </summary>
|
||||
public void SetApplicationInfo(WitAppInfo newInfo)
|
||||
{
|
||||
_appInfo = newInfo;
|
||||
SaveConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the plugin-specific data for this WitConfiguration
|
||||
/// </summary>
|
||||
public void SetConfigData(WitConfigurationAssetData[] configData)
|
||||
{
|
||||
_configData = configData;
|
||||
}
|
||||
// Save this configuration asset
|
||||
private void SaveConfiguration()
|
||||
{
|
||||
EditorUtility.SetDirty(this);
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
AssetDatabase.SaveAssetIfDirty(this);
|
||||
#else
|
||||
AssetDatabase.SaveAssets();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void RefreshPlugins()
|
||||
{
|
||||
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
// Find all derived data types
|
||||
List<Type> dataPlugins = typeof(WitConfigurationAssetData).GetSubclassTypes();
|
||||
|
||||
// Create instances of the types and register them
|
||||
List<WitConfigurationAssetData> newConfigs = new List<WitConfigurationAssetData>();
|
||||
var configurationAssetPath = AssetDatabase.GetAssetPath(this);
|
||||
foreach (Type dataType in dataPlugins)
|
||||
{
|
||||
|
||||
// Grab existing if present
|
||||
var plugin = (WitConfigurationAssetData)AssetDatabase.LoadAssetAtPath(configurationAssetPath, dataType);
|
||||
// Generate instance & add to asset
|
||||
if (plugin == null)
|
||||
{
|
||||
plugin = (WitConfigurationAssetData)CreateInstance(dataType);
|
||||
plugin.name = dataType.Name;
|
||||
AssetDatabase.AddObjectToAsset(plugin, configurationAssetPath);
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(plugin));
|
||||
plugin = (WitConfigurationAssetData)AssetDatabase.LoadAssetAtPath(configurationAssetPath, dataType);
|
||||
}
|
||||
newConfigs.Add(plugin);
|
||||
}
|
||||
SetConfigData(newConfigs.ToArray());
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
#endif
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae5a46bda3295124c99b0e6537ac7252
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
|
||||
namespace Meta.WitAi.Configuration
|
||||
{
|
||||
[Serializable]
|
||||
public class WitEndpointConfig : IWitRequestEndpointInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Customized uri scheme (Ex. https)
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("uriScheme")]
|
||||
private string _uriScheme;
|
||||
public string UriScheme => string.IsNullOrEmpty(_uriScheme) ? WitConstants.URI_SCHEME : _uriScheme;
|
||||
|
||||
/// <summary>
|
||||
/// Customized host location (Ex. api.wit.ai)
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("authority")]
|
||||
private string _authority;
|
||||
public string Authority =>
|
||||
string.IsNullOrEmpty(_authority) ? WitConstants.URI_AUTHORITY : _authority;
|
||||
|
||||
/// <summary>
|
||||
/// Customized host port (Ex. api.wit.ai)
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("port")]
|
||||
private int _port;
|
||||
public int Port => _port <= 0 ? WitConstants.URI_DEFAULT_PORT : _port;
|
||||
|
||||
/// <summary>
|
||||
/// API version to be used for this endpoint. Defaults to sdk default version
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("witApiVersion")]
|
||||
private string _witApiVersion;
|
||||
public string WitApiVersion => string.IsNullOrEmpty(_witApiVersion)
|
||||
? WitConstants.API_VERSION
|
||||
: _witApiVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used for text based voice command. Defaults to 'message'
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("message")]
|
||||
private string _message;
|
||||
public string Message =>
|
||||
string.IsNullOrEmpty(_message) ? WitConstants.ENDPOINT_MESSAGE : _message;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used for audio based voice command. Defaults to 'speech'
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("speech")]
|
||||
private string _speech;
|
||||
public string Speech =>
|
||||
string.IsNullOrEmpty(_speech) ? WitConstants.ENDPOINT_SPEECH : _speech;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used for audio based transcription. Defaults to 'dictation'
|
||||
/// </summary>
|
||||
[SerializeField] [FormerlySerializedAs("dictation")]
|
||||
private string _dictation;
|
||||
public string Dictation => string.IsNullOrEmpty(_dictation) ? WitConstants.ENDPOINT_DICTATION : _dictation;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used for Text-To-Speech. Defaults to 'synthesize'
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private string _synthesize;
|
||||
public string Synthesize => string.IsNullOrEmpty(_synthesize) ? WitConstants.ENDPOINT_TTS : _synthesize;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used for Composer text requests. Defaults to 'event'
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private string _event;
|
||||
public string Event => string.IsNullOrEmpty(_event) ? WitConstants.ENDPOINT_COMPOSER_MESSAGE : _event;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used for Composer audio requests. Defaults to 'converse'
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private string _converse;
|
||||
public string Converse => string.IsNullOrEmpty(_converse) ? WitConstants.ENDPOINT_COMPOSER_SPEECH : _converse;
|
||||
|
||||
// Default endpoint data
|
||||
private static WitEndpointConfig defaultEndpointConfig = new WitEndpointConfig();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a configuration using a preset if possible
|
||||
/// </summary>
|
||||
public static WitEndpointConfig GetEndpointConfig(WitConfiguration witConfig)
|
||||
{
|
||||
return witConfig && null != witConfig.endpointConfiguration
|
||||
? witConfig.endpointConfiguration
|
||||
: defaultEndpointConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61a0415bfdfa4d64e89801fbc0b30ec7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Meta.WitAi.Json;
|
||||
using Meta.WitAi.Requests;
|
||||
using Meta.WitAi.Interfaces;
|
||||
|
||||
namespace Meta.WitAi.Configuration
|
||||
{
|
||||
public class WitRequestOptions : VoiceServiceRequestOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface that provides a list of entities that should be used for nlu resolution.
|
||||
/// </summary>
|
||||
public IDynamicEntitiesProvider dynamicEntities;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of intent matches to return
|
||||
/// </summary>
|
||||
public int nBestIntents = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The tag for snapshot
|
||||
/// </summary>
|
||||
public string tag;
|
||||
|
||||
/// <summary>
|
||||
/// Formerly used for request id
|
||||
/// </summary>
|
||||
[Obsolete("Use 'RequestId' property instead")] [JsonIgnore]
|
||||
public string requestID => RequestId;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for completion
|
||||
/// </summary>
|
||||
public Action<WitRequest> onResponse;
|
||||
|
||||
// Get json string. Used to get the payload for PI.
|
||||
// PI will reparse these parameters and construct it's own request.
|
||||
public string ToJsonString()
|
||||
{
|
||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||
parameters["nBestIntents"] = nBestIntents.ToString();
|
||||
parameters["tag"] = tag;
|
||||
parameters["requestID"] = RequestId;
|
||||
foreach (var key in QueryParams.Keys)
|
||||
{
|
||||
parameters[key] = QueryParams[key];
|
||||
}
|
||||
return JsonConvert.SerializeObject(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6834fd3c55fb41a1885302e6043f5d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
using Meta.WitAi.Interfaces;
|
||||
using Meta.WitAi.Utilities;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Meta.WitAi.Configuration
|
||||
{
|
||||
[Serializable]
|
||||
public class WitRuntimeConfiguration
|
||||
{
|
||||
[Tooltip("Configuration for the application used in this instance of Wit.ai services")]
|
||||
[SerializeField]
|
||||
public WitConfiguration witConfiguration;
|
||||
|
||||
[Header("Keepalive")]
|
||||
[Tooltip("The minimum volume from the mic needed to keep the activation alive")]
|
||||
[SerializeField]
|
||||
public float minKeepAliveVolume = .0005f;
|
||||
|
||||
[FormerlySerializedAs("minKeepAliveTime")]
|
||||
[Tooltip(
|
||||
"The amount of time in seconds an activation will be kept open after volume is under the keep alive threshold")]
|
||||
[SerializeField]
|
||||
[DynamicRange("RecordingTimeRange")]
|
||||
public float minKeepAliveTimeInSeconds = 2f;
|
||||
|
||||
[FormerlySerializedAs("minTranscriptionKeepAliveTime")]
|
||||
[Tooltip(
|
||||
"The amount of time in seconds an activation will be kept open after words have been detected in the live transcription")]
|
||||
[SerializeField]
|
||||
[DynamicRange("RecordingTimeRange")]
|
||||
public float minTranscriptionKeepAliveTimeInSeconds = 1f;
|
||||
|
||||
[Tooltip("The maximum amount of time in seconds the mic will stay active")]
|
||||
[SerializeField]
|
||||
[DynamicRange("RecordingTimeRange")]
|
||||
public float maxRecordingTime = 20;
|
||||
|
||||
protected virtual Vector2 RecordingTimeRange => new Vector2(0, 20);
|
||||
|
||||
[Header("Sound Activation")]
|
||||
[Tooltip("The minimum volume level needed to be heard to start collecting data from the audio source.")]
|
||||
[SerializeField] public float soundWakeThreshold = .0005f;
|
||||
|
||||
[Tooltip("The length of the individual samples read from the audio source")]
|
||||
[Range(10, 500)] [SerializeField] public int sampleLengthInMs = 10;
|
||||
|
||||
[Tooltip("The total audio data that should be buffered for lookback purposes on sound based activations.")]
|
||||
[SerializeField] public float micBufferLengthInSeconds = 1;
|
||||
|
||||
[Tooltip("The maximum amount of concurrent requests that can occur")]
|
||||
[Range(1, 10)] [SerializeField] public int maxConcurrentRequests = 5;
|
||||
|
||||
[Header("Custom Transcription")]
|
||||
[Tooltip(
|
||||
"If true, the audio recorded in the activation will be sent to Wit.ai for processing. If a custom transcription provider is set and this is false, only the transcription will be sent to Wit.ai for processing")]
|
||||
[SerializeField]
|
||||
public bool sendAudioToWit = true;
|
||||
|
||||
[Tooltip("A custom provider that returns text to be used for nlu processing on activation instead of sending audio.")]
|
||||
[SerializeField] public CustomTranscriptionProvider customTranscriptionProvider;
|
||||
|
||||
[Tooltip("If always record is set the mic will fill the mic data buffer as long as the component is enabled in the scene.")]
|
||||
public bool alwaysRecord;
|
||||
|
||||
[Tooltip("The preferred number of seconds to offset from the time the activation happens. A negative value here could help to catch any words that may have been cut off at the beginning of an activation (assuming input is already being read into the buffer)")]
|
||||
public float preferredActivationOffset = -.5f;
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd8d40c9f75b44b5940fcc21769d6af0
|
||||
timeCreated: 1629397017
|
||||
Reference in New Issue
Block a user