From 40fd4089084780a8156dbcc2415aa599b148c4b4 Mon Sep 17 00:00:00 2001 From: "tom.hempel" Date: Fri, 26 Sep 2025 16:40:21 +0200 Subject: [PATCH] final fixes for audio + installed parallelsync --- Unity-Master/Assets/Plugins/ParrelSync.meta | 8 + .../Plugins/ParrelSync/ScriptableObjects.meta | 8 + .../ParrelSyncProjectSettings.asset | 3 + .../ParrelSyncProjectSettings.asset.meta | 8 + Unity-Master/Assets/Scenes/VR_Player2.unity | 4 +- .../ConvaiSimpleUDPAudioReceiver.cs | 250 ++++++++++++++---- .../Multiplayer/ConvaiSimpleUDPAudioSender.cs | 122 +++++++++ .../Assets/Scripts/NetworkConfig.asset | 4 +- Unity-Master/Packages/manifest.json | 1 + Unity-Master/Packages/packages-lock.json | 7 + 10 files changed, 365 insertions(+), 50 deletions(-) create mode 100644 Unity-Master/Assets/Plugins/ParrelSync.meta create mode 100644 Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects.meta create mode 100644 Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset create mode 100644 Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta diff --git a/Unity-Master/Assets/Plugins/ParrelSync.meta b/Unity-Master/Assets/Plugins/ParrelSync.meta new file mode 100644 index 0000000..4b87df0 --- /dev/null +++ b/Unity-Master/Assets/Plugins/ParrelSync.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bdf5ff7ba76a952439899cd468855cdb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects.meta b/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects.meta new file mode 100644 index 0000000..df58db4 --- /dev/null +++ b/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 53e7ebc83181ffc4eb83d06a00cda31d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset b/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset new file mode 100644 index 0000000..55632f2 --- /dev/null +++ b/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:055b03f3b4a258445d806f20c1054454d337d66913a3dd9e462f76cec521e7bc +size 450 diff --git a/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta b/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta new file mode 100644 index 0000000..69e1747 --- /dev/null +++ b/Unity-Master/Assets/Plugins/ParrelSync/ScriptableObjects/ParrelSyncProjectSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 065ee3e807568e04b8e6bfd81da61980 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-Master/Assets/Scenes/VR_Player2.unity b/Unity-Master/Assets/Scenes/VR_Player2.unity index a00737f..ff75ebf 100644 --- a/Unity-Master/Assets/Scenes/VR_Player2.unity +++ b/Unity-Master/Assets/Scenes/VR_Player2.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5251ad7b0b163fb4778fd7b9ea3a9ab87994292ffee4e85190e0730c9ff42704 -size 4490772 +oid sha256:5145690491a5812a2ebdef849e1aa792df5c21ccd8ea1db742c79423e467a322 +size 4491068 diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs index b0b7bef..24bddd6 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs @@ -8,11 +8,12 @@ using Convai.Scripts.Runtime.Core; using Convai.Scripts.Runtime.LoggerSystem; using Convai.Scripts.Runtime.Utils; using UnityEngine; +using System.IO; namespace Convai.Scripts.Runtime.Multiplayer { /// - /// Simple UDP Audio Receiver - Simulates microphone input by triggering normal Convai flow + /// Simple UDP Audio Receiver V2 - Simulates microphone input by triggering normal Convai flow /// This approach is much simpler and more reliable than trying to replicate gRPC calls /// public class ConvaiSimpleUDPAudioReceiver : MonoBehaviour @@ -39,27 +40,44 @@ namespace Convai.Scripts.Runtime.Multiplayer // Audio state tracking private bool _isReceivingAudio = false; private int _expectedSequence = 0; - private const uint AUDIO_MAGIC = 0xC0A1; // Audio packet magic - private const uint ACK_MAGIC = 0xC0A2; // Ack packet magic + private const uint MAGIC_NUMBER = 0xC0A1; // Simple magic number for packet validation + private const uint ACK_MAGIC = 0xC0A2; // ACK magic to confirm START control // Timing for auto-stop private float _lastPacketTime; private const float AUTO_STOP_DELAY = 1.0f; // Stop listening after 1 second of no packets + // Packet structure (matching ConvaiSimpleUDPAudioSender) private struct AudioPacketData { + public uint magicNumber; public int sequence; public int sampleCount; public int microphonePosition; public bool isEndSignal; public bool isStartSignal; public short[] audioSamples; + public long timestamp; } + [Header("Recording Storage")] + [SerializeField] private bool saveReceivedAudio = true; + [SerializeField] private int receivedSampleRate = 16000; // Should match sender + [SerializeField] private string outputFilePrefix = "received_audio"; + + private readonly object _audioBufferLock = new object(); + private List _receivedSamples = new List(64 * 1024); + private Dictionary _pendingPackets = new Dictionary(); + private int _nextSequenceToWrite = 0; + private DateTime _sessionStartTime; + private bool _saveInProgress = false; + private string _persistentDataPath; + private void Start() { _cancellationTokenSource = new CancellationTokenSource(); + _persistentDataPath = Application.persistentDataPath; // Apply global config if enabled if (useGlobalNetworkConfig) { @@ -131,13 +149,8 @@ namespace Convai.Scripts.Runtime.Multiplayer private void InitializeConvai() { - // Prefer local ConvaiNPC on the same GameObject, then fall back to active NPC - var localNPC = GetComponent(); - if (localNPC != null) - { - targetNPC = localNPC; - } - else if (useActiveNPC) + // Get target NPC + if (useActiveNPC) { targetNPC = ConvaiNPCManager.Instance?.GetActiveConvaiNPC(); } @@ -216,9 +229,9 @@ namespace Convai.Scripts.Runtime.Multiplayer { try { - var packetData = ParseSimpleAudioPacket(data, sender); + var packetData = ParseSimpleAudioPacket(data); - if (packetData.HasValue) + if (packetData.HasValue) { var packet = packetData.Value; _lastPacketTime = Time.time; @@ -231,23 +244,37 @@ namespace Convai.Scripts.Runtime.Multiplayer ConvaiLogger.DebugLog($"Received audio packet {packet.sequence} with {packet.sampleCount} samples", ConvaiLogger.LogCategory.Character); } - if (packet.isEndSignal) + // Handle START control: acknowledge and begin simulation + if (packet.isStartSignal) + { + SendStartAck(sender); + if (!_isReceivingAudio) + { + StartTalkingSimulation(); + } + OnAudioReceiving?.Invoke(true); + return; + } + + if (packet.isEndSignal) { StopTalkingSimulation(); OnAudioReceiving?.Invoke(false); } else { - if (packet.isStartSignal) - { - // START packet acknowledged earlier - } // If this is the first packet, start the talking simulation if (packet.sequence == 0 && !_isReceivingAudio) { StartTalkingSimulation(); } + // Buffer audio samples for saving + if (packet.audioSamples != null && packet.audioSamples.Length > 0) + { + BufferAudioPacket(packet.sequence, packet.audioSamples); + } + OnAudioReceiving?.Invoke(true); } } @@ -255,7 +282,8 @@ namespace Convai.Scripts.Runtime.Multiplayer { // Not our audio packet format, might be a test message string message = System.Text.Encoding.UTF8.GetString(data); - ConvaiLogger.Info($"Received test message from {sender}: {message}", ConvaiLogger.LogCategory.Character); + if (enableDebugLogging) + ConvaiLogger.Info($"Received test message from {sender}: {message}", ConvaiLogger.LogCategory.Character); } } catch (Exception ex) @@ -283,6 +311,13 @@ namespace Convai.Scripts.Runtime.Multiplayer _isReceivingAudio = true; _expectedSequence = 0; + _nextSequenceToWrite = 0; + _sessionStartTime = DateTime.UtcNow; + lock (_audioBufferLock) + { + _receivedSamples.Clear(); + _pendingPackets.Clear(); + } // This is the KEY! Simulate a talk key press to trigger normal Convai flow ConvaiInputManager.Instance.talkKeyInteract?.Invoke(true); @@ -302,42 +337,47 @@ namespace Convai.Scripts.Runtime.Multiplayer ConvaiInputManager.Instance.talkKeyInteract?.Invoke(false); ConvaiLogger.Info($"🎤 Stopped talking simulation for {targetNPC?.characterName ?? "NPC"} (remote player audio)", ConvaiLogger.LogCategory.Character); + + if (saveReceivedAudio) + { + TrySaveReceivedAudioAsync(); + } }); } - private AudioPacketData? ParseSimpleAudioPacket(byte[] data, IPEndPoint sender) + private AudioPacketData? ParseSimpleAudioPacket(byte[] data) { - // Sender uses a 17-byte header (no timestamp/padding). We also support older 24+ byte format gracefully. - if (data.Length < 17) + if (data.Length < 17) // Minimum header size to match sender return null; try { int offset = 0; - + + // Read magic number uint magic = BitConverter.ToUInt32(data, offset); offset += 4; - if (magic != AUDIO_MAGIC) - { - // Might be a test message or something else + + if (magic != MAGIC_NUMBER) return null; - } - + + // Read header (matching sender's 17-byte format) int sequence = BitConverter.ToInt32(data, offset); offset += 4; + int sampleCount = BitConverter.ToInt32(data, offset); offset += 4; + int microphonePosition = BitConverter.ToInt32(data, offset); offset += 4; - byte flag = data[offset]; + + byte flags = data[offset]; offset += 1; - - bool isEndSignal = (flag == 1); - bool isStartSignal = (flag == 2); - - // Send ACK immediately (for START and audio packets) - SendAck(sender, sequence); - + + bool isEndSignal = (flags == 1); + bool isStartSignal = (flags == 2); + + // Read audio data short[] audioSamples = null; if (!isEndSignal && !isStartSignal && sampleCount > 0) { @@ -348,15 +388,17 @@ namespace Convai.Scripts.Runtime.Multiplayer Buffer.BlockCopy(data, offset, audioSamples, 0, audioDataSize); } } - + return new AudioPacketData { + magicNumber = magic, sequence = sequence, sampleCount = sampleCount, microphonePosition = microphonePosition, isEndSignal = isEndSignal, isStartSignal = isStartSignal, - audioSamples = audioSamples + audioSamples = audioSamples, + timestamp = 0 // Not provided in sender format }; } catch (Exception ex) @@ -366,21 +408,137 @@ namespace Convai.Scripts.Runtime.Multiplayer } } - private void SendAck(IPEndPoint recipient, int sequence) + private void SendStartAck(IPEndPoint sender) { try { - using (var client = new UdpClient()) - { - byte[] ack = new byte[8]; - Buffer.BlockCopy(BitConverter.GetBytes(ACK_MAGIC), 0, ack, 0, 4); - Buffer.BlockCopy(BitConverter.GetBytes(sequence), 0, ack, 4, 4); - client.Send(ack, ack.Length, recipient); - } + if (_udpListener == null || sender == null) + return; + + byte[] ack = new byte[8]; + BitConverter.GetBytes(ACK_MAGIC).CopyTo(ack, 0); + BitConverter.GetBytes(-1).CopyTo(ack, 4); + _udpListener.SendAsync(ack, ack.Length, sender); + + if (enableDebugLogging) + ConvaiLogger.DebugLog($"Sent START ACK to {sender}", ConvaiLogger.LogCategory.Character); } catch (Exception ex) { - ConvaiLogger.Warn($"Failed to send ACK: {ex.Message}", ConvaiLogger.LogCategory.Character); + ConvaiLogger.Warn($"Failed to send START ACK: {ex.Message}", ConvaiLogger.LogCategory.Character); + } + } + + private void BufferAudioPacket(int sequence, short[] samples) + { + if (samples == null || samples.Length == 0) + return; + + lock (_audioBufferLock) + { + if (sequence < _nextSequenceToWrite) + { + return; // old/duplicate packet + } + + if (sequence == _nextSequenceToWrite) + { + _receivedSamples.AddRange(samples); + _nextSequenceToWrite++; + + // Flush any contiguous pending packets + while (_pendingPackets.TryGetValue(_nextSequenceToWrite, out var nextSamples)) + { + _receivedSamples.AddRange(nextSamples); + _pendingPackets.Remove(_nextSequenceToWrite); + _nextSequenceToWrite++; + } + } + else + { + // Store for later when gap is filled + _pendingPackets[sequence] = samples; + } + } + } + + private void TrySaveReceivedAudioAsync() + { + if (_saveInProgress) + return; + + short[] dataToSave; + DateTime sessionStart; + lock (_audioBufferLock) + { + if (_receivedSamples == null || _receivedSamples.Count == 0) + { + if (enableDebugLogging) + ConvaiLogger.Info("No received audio to save.", ConvaiLogger.LogCategory.Character); + return; + } + dataToSave = _receivedSamples.ToArray(); + _receivedSamples.Clear(); + _pendingPackets.Clear(); + sessionStart = _sessionStartTime; + } + + _saveInProgress = true; + Task.Run(() => + { + try + { + string timestamp = sessionStart.ToLocalTime().ToString("yyyyMMdd_HHmmss"); + string fileName = $"{outputFilePrefix}_{timestamp}.wav"; + string dir = _persistentDataPath; + string path = Path.Combine(dir, fileName); + WriteWav(path, dataToSave, receivedSampleRate, 1); + ConvaiLogger.Info($"Saved received audio to: {path}", ConvaiLogger.LogCategory.Character); + } + catch (Exception ex) + { + ConvaiLogger.Error($"Failed to save received audio: {ex.Message}", ConvaiLogger.LogCategory.Character); + } + finally + { + _saveInProgress = false; + } + }); + } + + private void WriteWav(string path, short[] samples, int sampleRate, int channels) + { + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var writer = new BinaryWriter(fs)) + { + int bitsPerSample = 16; + int byteRate = sampleRate * channels * (bitsPerSample / 8); + int blockAlign = channels * (bitsPerSample / 8); + int dataSize = samples.Length * (bitsPerSample / 8); + int fileSize = 44 - 8 + dataSize; + + // RIFF header + writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF")); + writer.Write(fileSize); + writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVE")); + + // fmt chunk + writer.Write(System.Text.Encoding.ASCII.GetBytes("fmt ")); + writer.Write(16); // Subchunk1Size for PCM + writer.Write((short)1); // AudioFormat = PCM + writer.Write((short)channels); // NumChannels + writer.Write(sampleRate); // SampleRate + writer.Write(byteRate); // ByteRate + writer.Write((short)blockAlign); // BlockAlign + writer.Write((short)bitsPerSample); // BitsPerSample + + // data chunk + writer.Write(System.Text.Encoding.ASCII.GetBytes("data")); + writer.Write(dataSize); + for (int i = 0; i < samples.Length; i++) + { + writer.Write(samples[i]); + } } } diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs index 6df8f74..af82628 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs @@ -9,6 +9,7 @@ using UnityEngine; using UnityEngine.XR; using UnityEngine.InputSystem; using UnityEngine.InputSystem.XR; +using System.IO; namespace Convai.Scripts.Runtime.Multiplayer { @@ -70,6 +71,16 @@ namespace Convai.Scripts.Runtime.Multiplayer public event Action OnRecordingStateChanged; + [Header("Recording Storage")] + [SerializeField] private bool saveLocalAudio = true; + [SerializeField] private int localSampleRate = 16000; + [SerializeField] private string localFilePrefix = "sender_audio"; + private readonly object _localAudioLock = new object(); + private readonly System.Collections.Generic.List _localSamples = new System.Collections.Generic.List(128 * 1024); + private bool _localSaveInProgress = false; + private DateTime _localSessionStart; + private string _persistentDataPath; + private void Start() { // Apply global config if enabled @@ -84,6 +95,7 @@ namespace Convai.Scripts.Runtime.Multiplayer } InitializeNetwork(); InitializeAudio(); + _persistentDataPath = Application.persistentDataPath; _cancellationTokenSource = new CancellationTokenSource(); _ackCancellationTokenSource = new CancellationTokenSource(); @@ -423,6 +435,11 @@ namespace Convai.Scripts.Runtime.Multiplayer _lastMicrophonePosition = 0; _packetSequence = 0; _startAckReceived = false; + _localSessionStart = DateTime.UtcNow; + lock (_localAudioLock) + { + _localSamples.Clear(); + } ConvaiLogger.Info("Started recording for UDP transmission (Simple)", ConvaiLogger.LogCategory.Character); OnRecordingStateChanged?.Invoke(true); @@ -454,6 +471,11 @@ namespace Convai.Scripts.Runtime.Multiplayer // Send end-of-recording signal SendEndOfRecordingSignal(); + + if (saveLocalAudio) + { + TrySaveLocalAudioAsync(); + } } catch (Exception ex) { @@ -542,6 +564,12 @@ namespace Convai.Scripts.Runtime.Multiplayer // Create a simple packet structure byte[] packet = CreateSimpleAudioPacket(audioData, processedSamples, currentChunkSamples); + // Buffer locally for saving + if (saveLocalAudio) + { + AppendLocalAudio(audioData, processedSamples, currentChunkSamples); + } + // Send the packet await _udpClient.SendAsync(packet, packet.Length, _targetEndPoint); @@ -649,6 +677,100 @@ namespace Convai.Scripts.Runtime.Multiplayer } } + private void AppendLocalAudio(float[] source, int startIndex, int count) + { + if (source == null || count <= 0) + return; + + lock (_localAudioLock) + { + for (int i = 0; i < count; i++) + { + float sample = source[startIndex + i]; + short shortSample = (short)(Mathf.Clamp(sample, -1f, 1f) * short.MaxValue); + _localSamples.Add(shortSample); + } + } + } + + private void TrySaveLocalAudioAsync() + { + if (_localSaveInProgress) + return; + + short[] dataToSave; + DateTime sessionStart; + lock (_localAudioLock) + { + if (_localSamples.Count == 0) + { + if (enableDebugLogging) + ConvaiLogger.Info("No local audio to save.", ConvaiLogger.LogCategory.Character); + return; + } + dataToSave = _localSamples.ToArray(); + _localSamples.Clear(); + sessionStart = _localSessionStart; + } + + _localSaveInProgress = true; + Task.Run(async () => + { + try + { + // Small delay to allow any final chunks to enqueue + await Task.Delay(100); + string timestamp = sessionStart.ToLocalTime().ToString("yyyyMMdd_HHmmss"); + string fileName = $"{localFilePrefix}_{timestamp}.wav"; + string dir = _persistentDataPath; + string path = Path.Combine(dir, fileName); + WriteWav(path, dataToSave, localSampleRate, 1); + ConvaiLogger.Info($"Saved local audio to: {path}", ConvaiLogger.LogCategory.Character); + } + catch (Exception ex) + { + ConvaiLogger.Error($"Failed to save local audio: {ex.Message}", ConvaiLogger.LogCategory.Character); + } + finally + { + _localSaveInProgress = false; + } + }); + } + + private void WriteWav(string path, short[] samples, int sampleRate, int channels) + { + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var writer = new BinaryWriter(fs)) + { + int bitsPerSample = 16; + int byteRate = sampleRate * channels * (bitsPerSample / 8); + int blockAlign = channels * (bitsPerSample / 8); + int dataSize = samples.Length * (bitsPerSample / 8); + int fileSize = 44 - 8 + dataSize; + + writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF")); + writer.Write(fileSize); + writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVE")); + + writer.Write(System.Text.Encoding.ASCII.GetBytes("fmt ")); + writer.Write(16); + writer.Write((short)1); + writer.Write((short)channels); + writer.Write(sampleRate); + writer.Write(byteRate); + writer.Write((short)blockAlign); + writer.Write((short)bitsPerSample); + + writer.Write(System.Text.Encoding.ASCII.GetBytes("data")); + writer.Write(dataSize); + for (int i = 0; i < samples.Length; i++) + { + writer.Write(samples[i]); + } + } + } + private async Task SendStartOfRecordingSignalAndAwaitAck() { try diff --git a/Unity-Master/Assets/Scripts/NetworkConfig.asset b/Unity-Master/Assets/Scripts/NetworkConfig.asset index baa203c..66df588 100644 --- a/Unity-Master/Assets/Scripts/NetworkConfig.asset +++ b/Unity-Master/Assets/Scripts/NetworkConfig.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79445491a897bce57a20454b758ae53e213754ff494e89faddc53b6db5584d28 -size 503 +oid sha256:397abf447ea7157bbda652c7c87d6d0527f4af64306f1200fc969c0c6782acd2 +size 505 diff --git a/Unity-Master/Packages/manifest.json b/Unity-Master/Packages/manifest.json index 78129dd..9a98982 100644 --- a/Unity-Master/Packages/manifest.json +++ b/Unity-Master/Packages/manifest.json @@ -34,6 +34,7 @@ "com.unity.xr.interaction.toolkit": "2.6.4", "com.unity.xr.management": "4.5.1", "com.unity.xr.openxr": "1.12.1", + "com.veriorpies.parrelsync": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Unity-Master/Packages/packages-lock.json b/Unity-Master/Packages/packages-lock.json index dd991c2..44d761c 100644 --- a/Unity-Master/Packages/packages-lock.json +++ b/Unity-Master/Packages/packages-lock.json @@ -398,6 +398,13 @@ }, "url": "https://packages.unity.com" }, + "com.veriorpies.parrelsync": { + "version": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "610157ad762084380380148ba8ce14e266a6da97" + }, "com.unity.modules.ai": { "version": "1.0.0", "depth": 0,