#if UNITY_EDITOR ////////////////////////////////////////////////////////////////////// // // Copyright (c) 2020 Audiokinetic Inc. / All Rights Reserved // ////////////////////////////////////////////////////////////////////// using System.Linq; using System.Collections.Generic; using System.Collections.Concurrent; using System.Collections.ObjectModel; using UnityEditor.IMGUI.Controls; /// /// This class communicates with Wwise Authoring via AkWaapiUtilities to keep track of the Wwise object hierarchy in the project. /// This hierarchy information is stored in a tree structure and is used by the Wwise Picker when it is in WAAPI mode. /// Changes to the project are received via WAAPI subscriptions. /// public class AkWwiseTreeWAAPIDataSource : AkWwiseTreeDataSource { private System.Timers.Timer selectTimer; private System.Timers.Timer searchTimer; private ReturnOptions waapiWwiseObjectOptions = new ReturnOptions(new string[] { "id", "name", "type", "childrenCount", "path", "workunitType", "parent" }); public bool AutoSyncSelection; public bool WaitingForSearchResults; public AkWwiseTreeWAAPIDataSource() : base() { Connect(); selectTimer = new System.Timers.Timer(); selectTimer.Interval = 200; selectTimer.AutoReset = false; selectTimer.Elapsed += FireSelect; searchTimer = new System.Timers.Timer(); searchTimer.Interval = 200; searchTimer.AutoReset = false; searchTimer.Elapsed += FireSearch; } public override void FetchData() { Data.Clear(); m_MaxID = 0; ProjectRoot = CreateProjectRootItem(); foreach (var type in FolderNames.Keys) { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { AddBaseFolder(AkWaapiUtilities.ParseObjectInfo(items), type); }; AkWaapiUtilities.GetWwiseObjectAndDescendants(FolderNames[type], waapiWwiseObjectOptions, 2, callback); } Changed(); } public override AkWwiseTreeViewItem GetComponentDataRoot(WwiseObjectType objectType) { var tempProjectRoot = new AkWwiseTreeViewItem(ProjectRoot); if (!wwiseObjectFolders.ContainsKey(objectType)) { return tempProjectRoot; } tempProjectRoot.AddWwiseItemChild(wwiseObjectFolders[objectType]); return tempProjectRoot; } WwiseObjectType componentObjectType; public override void LoadComponentData(WwiseObjectType objectType) { componentObjectType = objectType; LoadComponentDataDelayed(); } public void LoadComponentDataDelayed() { //Delay call until data has been fetched if (!wwiseObjectFolders.ContainsKey(componentObjectType)) { UnityEditor.EditorApplication.delayCall += LoadComponentDataDelayed; } else { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { AddItems(AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetWwiseObjectAndDescendants(wwiseObjectFolders[componentObjectType].objectGuid, waapiWwiseObjectOptions, -1, callback); } } private string searchString; private WwiseObjectType searchObjectTypeFilter; public override void UpdateSearchResults(string searchFilter, WwiseObjectType objectType = WwiseObjectType.None) { searchTimer.Stop(); searchString = searchFilter; searchObjectTypeFilter = objectType; searchTimer.Enabled = true; searchTimer.Start(); } private void FireSearch(object sender, System.Timers.ElapsedEventArgs e) { if (WaitingForSearchResults) return; if (SearchRoot == null) { SearchRoot = new AkWwiseTreeViewItem(ProjectRoot); } SearchRoot.children.Clear(); SearchData = new TreeItems(); TreeUtility.TreeToList(SearchRoot, ref SearchData); AkWaapiUtilities.GetResultListDelegate callback = (List items) => { AddSearchResults(AkWaapiUtilities.ParseObjectInfo(items)); treeviewCommandQueue.Enqueue(new TreeViewCommand(() => Expand(SearchRoot.objectGuid, false))); WaitingForSearchResults = false; }; AkWaapiUtilities.Search(searchString, searchObjectTypeFilter, waapiWwiseObjectOptions, callback); WaitingForSearchResults = true; } public override AkWwiseTreeViewItem GetSearchResults() { if (SearchRoot == null) { SearchRoot = new AkWwiseTreeViewItem(ProjectRoot); } return SearchRoot; } public void AddSearchResults(IEnumerable matchList) { try { foreach (var info in matchList) { if (!FilterPath(info.path)) { continue; } var match = FindInSearchResults(info.objectGUID); if (match != null) { continue; } var matchItem = FindByGuid(info.objectGUID); if (matchItem == null) { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { AddItemWithAncestorsToSearch(AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetWwiseObjectAndAncestors(info.objectGUID, waapiWwiseObjectOptions, callback); continue; } AddItemToSearch(matchItem); } } catch (System.Exception e) { UnityEngine.Debug.LogError("Search died"); UnityEngine.Debug.LogError(e.Message); } } private void AddItemToSearch(AkWwiseTreeViewItem sourceItem) { var matchItem = new AkWwiseTreeViewItem(sourceItem); SearchData.Add(matchItem); var sourceParent = sourceItem.parent as AkWwiseTreeViewItem; var parentCopy = FindInSearchResults(sourceParent.objectGuid); if (parentCopy != null) { parentCopy.AddWwiseItemChild(matchItem); return; } else { parentCopy = new AkWwiseTreeViewItem(sourceParent); parentCopy.AddWwiseItemChild(matchItem); SearchData.Add(parentCopy); var nextParent = sourceParent.parent as AkWwiseTreeViewItem; while (nextParent != null) { var parentInSearchItems = FindInSearchResults(nextParent.objectGuid); if (parentInSearchItems != null) { parentInSearchItems.AddWwiseItemChild(parentCopy); break; } parentInSearchItems = new AkWwiseTreeViewItem(nextParent); parentInSearchItems.AddWwiseItemChild(parentCopy); SearchData.Add(parentInSearchItems); parentCopy = parentInSearchItems; nextParent = nextParent.parent as AkWwiseTreeViewItem; } } } public void AddItemWithAncestors(List infoItems, bool selectAfterCreated = false) { var parent = ProjectRoot; //Items obtained from the WAAPI call are sorted by path so we can simply iterate over them foreach (var infoItem in infoItems) { var newItem = FindByGuid(infoItem.objectGUID); if (newItem == null) { newItem = new AkWwiseTreeViewItem(infoItem, GenerateUniqueID(), parent.depth + 1); Data.Add(newItem); parent.AddWwiseItemChild(newItem); } if (!CheckIfFullyLoaded(parent)) { System.Guid guid = new System.Guid(parent.objectGuid.ToString()); AkWaapiUtilities.GetResultListDelegate callback = (List items) => { UpdateParentWithLoadedChildren(guid, AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetChildren(parent.objectGuid, waapiWwiseObjectOptions, callback); } parent = newItem; } if (selectAfterCreated) { treeviewCommandQueue.Enqueue(new TreeViewCommand(() => Expand(parent.objectGuid, true))); } } public void AddItemWithAncestorsToSearch(List infoItems) { AddItemWithAncestors(infoItems, false); var item = FindByGuid(infoItems.Last().objectGUID); AddItemToSearch(item); } public void AddItems(List infoItems) { foreach (var infoItem in infoItems) { if (infoItem.type == WwiseObjectType.None) { continue; } var tParent = FindByGuid(infoItem.parentID); if (tParent == null || tParent == ProjectRoot) { tParent = ProjectRoot; } var tChild = FindByGuid(infoItem.objectGUID); if (tChild == null) { tChild = new AkWwiseTreeViewItem(infoItem, GenerateUniqueID(), tParent.depth + 1); Data.Add(tChild); tParent.AddWwiseItemChild(tChild); } } } public void AddBaseFolder(List infoItems, WwiseObjectType oType) { if (infoItems != null && infoItems.Count > 0) { AddItems(infoItems); var folder = FindByGuid(infoItems[0].objectGUID); wwiseObjectFolders[oType] = folder; } } public void UpdateParentWithLoadedChildren(System.Guid parentGuid, List children) { if (children == null) { return; } var parent = FindByGuid(parentGuid); if (parent == null) { return; } parent.children.Remove(null); parent.numChildren = children.Count; if (parent.children.Count > children.Count) { parent.children.Clear(); } foreach (var child in children) { if (parent.children.Any(c => ((AkWwiseTreeViewItem)c).objectGuid == child.objectGUID)) continue; else parent.AddWwiseItemChild(new AkWwiseTreeViewItem(child, GenerateUniqueID(), parent.depth + 1)); } } public void SetChildren(System.Guid parentGuid, List children) { if (children == null) { return; } var parent = FindByGuid(parentGuid); parent.children.Clear(); parent.children.Remove(null); parent.numChildren = children.Count; foreach (var child in children) { parent.AddWwiseItemChild(new AkWwiseTreeViewItem(child, GenerateUniqueID(), parent.depth + 1)); } } bool CheckIfFullyLoaded(AkWwiseTreeViewItem item) { if (item == ProjectRoot) { return true; } if (item.objectType == WwiseObjectType.Event) { return true; } if (item.numChildren != item.children.Count) { return false; } if (item.children.Contains(null)) { return false; } return true; } private ReadOnlyDictionary FolderNames = new ReadOnlyDictionary(new Dictionary() { { WwiseObjectType.AuxBus , @"\Master-Mixer Hierarchy" }, { WwiseObjectType.Event , @"\Events" }, { WwiseObjectType.State, @"\States"}, { WwiseObjectType.StateGroup, @"\States"}, { WwiseObjectType.Soundbank, @"\SoundBanks"}, { WwiseObjectType.Switch, @"\Switches"}, { WwiseObjectType.SwitchGroup, @"\Switches"}, { WwiseObjectType.AcousticTexture, @"\Virtual Acoustics" }, { WwiseObjectType.GameParameter, @"\Game Parameters" }, }); static List subscriptions = new List(); /// /// Subscribes to nameChanged, childAdded, childRemoved, and selectionChanged WAAPI events in order to keep the picker in sync with the Wwise project explorer. /// public void SubscribeTopics() { AkWaapiUtilities.Subscribe(ak.wwise.core.@object.nameChanged, OnWaapiRenamed, SubscriptionHandshake); AkWaapiUtilities.Subscribe(ak.wwise.core.@object.childAdded, OnWaapiChildAdded, SubscriptionHandshake); AkWaapiUtilities.Subscribe(ak.wwise.core.@object.childRemoved, OnWaapiChildRemoved, SubscriptionHandshake); AkWaapiUtilities.Subscribe(ak.wwise.ui.selectionChanged, OnWwiseSelectionChanged, SubscriptionHandshake); } public void SubscriptionHandshake(AkWaapiUtilities.SubscriptionInfo sub) { subscriptions.Add(sub); } /// /// Unsubscribes from currently active subscriptions. /// void UnsubscribeTopics() { var tSubs = subscriptions; foreach (var sub in tSubs) { if (sub.SubscriptionId != 0) { AkWaapiUtilities.Unsubscribe(sub.SubscriptionId); } } subscriptions.Clear(); } void OnWaapiRenamed(string json) { var renamedItem = AkWaapiUtilities.ParseRenameObject(json); treeviewCommandQueue.Enqueue(new TreeViewCommand(() => Rename(renamedItem.objectInfo.objectGUID, renamedItem.newName))); } void OnWaapiChildAdded(string json) { var added = AkWaapiUtilities.ParseChildAddedOrRemoved(json); if (added.childInfo.type == WwiseObjectType.None) return; var parent = FindByGuid(added.parentInfo.objectGUID); // New object created, but parent is not loaded yet, so we can ignore it if (parent == null) { return; } var child = FindByGuid(added.childInfo.objectGUID); if (child == null) { child = new AkWwiseTreeViewItem(added.childInfo, GenerateUniqueID(), parent.depth + 1); } else { child.numChildren = added.childInfo.childrenCount; child.displayName = added.childInfo.name; } parent.AddWwiseItemChild(child); Data.Add(child); parent.numChildren = added.parentInfo.childrenCount; child.depth = parent.depth + 1; if (!CheckIfFullyLoaded(parent)) { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { UpdateParentWithLoadedChildren(parent.objectGuid, AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetChildren(parent.objectGuid, waapiWwiseObjectOptions, callback); } if (!CheckIfFullyLoaded(child)) { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { UpdateParentWithLoadedChildren(child.objectGuid, AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetChildren(child.objectGuid, waapiWwiseObjectOptions, callback); } ScheduleRebuild(); } void OnWaapiChildRemoved(string json) { var removed = AkWaapiUtilities.ParseChildAddedOrRemoved(json); toRequeue.Enqueue(new TreeViewCommand(() => Remove(removed.parentInfo, removed.childInfo))); } void OnWwiseSelectionChanged(string json) { if (AutoSyncSelection) { var objects = AkWaapiUtilities.ParseSelectedObjects(json); if (objects.Count > 0) { if (FilterPath(objects[0].path)) { treeviewCommandQueue.Enqueue(new TreeViewCommand(() => SelectItem(objects[0].objectGUID))); } } } } public void Rename(System.Guid objectGuid, string newName) { var item = FindByGuid(objectGuid); if (item != null) { item.name = newName; } else { toRequeue.Enqueue(new TreeViewCommand(() => Rename(objectGuid, newName))); } } public void Remove(WwiseObjectInfo parentInfo, WwiseObjectInfo childInfo) { var parent = FindByGuid(parentInfo.objectGUID); //Object removed, but it was never loaded so we can ignore it if (parent == null) { return; } parent.numChildren = parentInfo.childrenCount; var index = parent.children.FindIndex(el => ((AkWwiseTreeViewItem)el).objectGuid == childInfo.objectGUID); if (index != -1) { parent.children.RemoveAt(index); } } public void Expand(System.Guid objectGuid, bool select) { if (TreeView == null || !TreeView.ExpandItem(objectGuid, select)) { toRequeue.Enqueue(new TreeViewCommand(() => Expand(objectGuid, select))); } } public override void SelectItem(System.Guid itemGuid) { if (TreeView == null) { return; } if (TreeView.m_storedSearchString != string.Empty) { return; } if (!TreeView.ExpandItem(itemGuid, true)) { var item = FindByGuid(itemGuid); treeviewCommandQueue.Enqueue(new TreeViewCommand(() => Expand(itemGuid, true))); if (item == null) { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { AddItemWithAncestors(AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetWwiseObjectAndAncestors(itemGuid, waapiWwiseObjectOptions, callback); } } } public bool FilterPath(string path) { var splitpath = path.Split('\\'); if (splitpath.Length > 1) { var folder = @"\" + splitpath[1]; if (FolderNames.Values.Contains(folder) || WaapiKeywords.FolderDisplaynames.Values.Contains(folder)) { return true; } } return false; } public override void ItemSelected(AkWwiseTreeViewItem item) { if (AutoSyncSelection) { SelectObjectInAuthoring(item.objectGuid); } } private System.Guid guidToSelect; public override void SelectObjectInAuthoring(System.Guid objectGuid) { selectTimer.Stop(); guidToSelect = objectGuid; selectTimer.Enabled = true; selectTimer.Start(); } private void FireSelect(object sender, System.Timers.ElapsedEventArgs e) { AkWaapiUtilities.SelectObjectInAuthoring(guidToSelect); } //Make a single command queue private ConcurrentQueue treeviewCommandQueue = new ConcurrentQueue(); private Queue toRequeue = new Queue(); public class TreeViewCommand { public System.Action payload; public TreeViewCommand(System.Action payload) { this.payload = payload; } public void Execute() { payload.Invoke(); } } public override void ScheduleRebuild() { rebuildFlag = true; } private bool rebuildFlag = false; private bool refreshFlag = false; public void Update() { while (treeviewCommandQueue.Count > 0) { if (treeviewCommandQueue.TryDequeue(out TreeViewCommand cmd)) { cmd.Execute(); refreshFlag = true; } } while (toRequeue.Count > 0) { treeviewCommandQueue.Enqueue(toRequeue.Dequeue()); } //Preemptively load items in hierarchy that are close to being exposed ( up to grandchildren of unexpanded items) if (rebuildFlag) { TreeUtility.TreeToList(ProjectRoot, ref Data); if (TreeView != null) { Preload(ProjectRoot, TreeView.state); } refreshFlag = true; rebuildFlag = false; } //Updates treeView data and sets repaint flag if (refreshFlag) { Changed(); refreshFlag = false; } } void Preload(AkWwiseTreeViewItem parent, TreeViewState treeState) { if (parent == null) { return; } if (!CheckIfFullyLoaded(parent)) { AkWaapiUtilities.GetResultListDelegate callback = (List items) => { UpdateParentWithLoadedChildren(parent.objectGuid, AkWaapiUtilities.ParseObjectInfo(items)); }; AkWaapiUtilities.GetChildren(parent.objectGuid, waapiWwiseObjectOptions, callback); } //Preload one level of hidden items. if (IsExpanded(treeState, parent.id) || (parent.parent != null && IsExpanded(treeState, parent.parent.id)) || parent.id == ProjectRoot.id ) { foreach (AkWwiseTreeViewItem childItem in parent.children) { Preload(childItem, treeState); } } } public override void SetExpanded(IEnumerable ids) { if (TreeView != null) { foreach (var id in ids) { treeviewCommandQueue.Enqueue(new TreeViewCommand(() => Expand(id, false))); } TreeView.state.expandedIDs.Clear(); } } public void Connect() { AkWaapiUtilities.Connected += OnConnection; AkWaapiUtilities.QueueConsumed += ScheduleRebuild; AkWaapiUtilities.Disconnecting += Disconnect; } public void OnConnection() { SubscribeTopics(); FetchData(); } public void Disconnect(bool still_connected) { this.treeviewCommandQueue = new ConcurrentQueue(); if (ProjectRoot != null) ProjectRoot.children = new List(); if (still_connected) { UnsubscribeTopics(); } else { subscriptions.Clear(); } Changed(); } public void Cleanup() { subscriptions.Clear(); } ~AkWwiseTreeWAAPIDataSource() { Disconnect(true); } public override void SaveExpansionStatus(List expandedItems) { AkWwiseProjectInfo.GetData().ExpandedWaapiItemIds = expandedItems; } public override List LoadExpansionSatus() { return AkWwiseProjectInfo.GetData().ExpandedWaapiItemIds; } } #endif