diff --git a/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentData.cs b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentData.cs new file mode 100644 index 0000000..14c3427 --- /dev/null +++ b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentData.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using Elements.Core; +using FrooxEngine; + +namespace Obsidian; + +public class ComponentData +{ + public SlimList members; + + public Component component; + + public bool Submitted { get; private set; } + + public string MainName + { + get + { + return component.Name; + } + } + + public int MemberCount => members.Count; + + public ComponentData(Component component) + { + this.component = component; + } + + public void MarkSubmitted() + { + if (Submitted) + { + throw new InvalidOperationException("This item is already marked as submitted"); + } + Submitted = true; + } + + public void ClearSubmitted() + { + if (!Submitted) + { + throw new InvalidOperationException("This item isn't marked as submitted"); + } + Submitted = false; + } + + public void AddMember(ISyncMember member) + { + members.Add(member); + } + + public bool MatchesSearchParameters(List optionalTerms, List requiredTerms, List excludedTerms) + { + foreach (string excludedTerm in excludedTerms) + { + if (MatchesTerm(excludedTerm)) + { + return false; + } + } + foreach (string requiredTerm in requiredTerms) + { + if (!MatchesTerm(requiredTerm)) + { + return false; + } + } + if (requiredTerms.Count > 0) + { + return true; + } + if (optionalTerms.Count == 0) + { + return true; + } + foreach (string optionalTerm in optionalTerms) + { + if (MatchesTerm(optionalTerm)) + { + return true; + } + } + return false; + } + + public bool MatchesTerm(string term) + { + if (component != null) + { + if (ContainsTerm(component.Name, term)) + { + return true; + } + } + return false; + } + + private static bool ContainsTerm(string str, string term) + { + if (string.IsNullOrEmpty(str)) + { + return false; + } + return str.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0; + } + + public override string ToString() + { + return $"Name: {MainName}, ReferenceID: {component.ReferenceID}, Members: {members.Count}"; + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentDataFeedItem.cs b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentDataFeedItem.cs new file mode 100644 index 0000000..eae9470 --- /dev/null +++ b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentDataFeedItem.cs @@ -0,0 +1,24 @@ +using Elements.Core; +using FrooxEngine; + +namespace Obsidian; + +public class ComponentDataFeedItem : DataFeedItem +{ + public SlimList> Members; + + public ComponentData Data { get; private set; } + + public ComponentDataFeedItem(ComponentData componentData) + { + InitBase(componentData.component.ReferenceID.ToString(), null, null, componentData.MainName); + Data = componentData; + foreach (ISyncMember member in componentData.members) + { + DataFeedEntity dataFeedEntity = new DataFeedEntity(); + dataFeedEntity.InitBase(member.ReferenceID.ToString(), null, null, member.Name); + dataFeedEntity.InitEntity(member); + Members.Add(dataFeedEntity); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentDataResult.cs b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentDataResult.cs new file mode 100644 index 0000000..39b6dd8 --- /dev/null +++ b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentDataResult.cs @@ -0,0 +1,16 @@ +using FrooxEngine; + +namespace Obsidian; + +public readonly struct ComponentDataResult +{ + public readonly ComponentData data; + + public readonly DataFeedItemChange change; + + public ComponentDataResult(ComponentData data, DataFeedItemChange change) + { + this.data = data; + this.change = change; + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentsDataFeed.cs b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentsDataFeed.cs new file mode 100644 index 0000000..df1dad4 --- /dev/null +++ b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentsDataFeed.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using Elements.Core; +using FrooxEngine; +using SkyFrost.Base; + +namespace Obsidian; + +[Category(new string[] { "Obsidian/Radiant UI/Data Feeds/Feeds" })] +public class ComponentsDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement +{ + private Dictionary _updateHandlers = new Dictionary(); + + public bool SupportsBackgroundQuerying => true; + + public readonly SyncRef TargetSlot; + + public readonly Sync IncludeChildrenSlots; + + private Slot _lastSlot = null; + + private void AddComponent(Component c) + { + // If local elements are written to synced fields it can cause exceptions and crashes + if (c.IsLocalElement) return; + foreach (KeyValuePair updateHandler in _updateHandlers) + { + var data = updateHandler.Value.RegisterComponent(c); + foreach (ISyncMember syncMember in data.component.SyncMembers) + { + if (syncMember.IsLocalElement) continue; + data.AddMember(syncMember); + } + ProcessUpdate(updateHandler.Key, data); + } + } + + private void RemoveComponent(Component c) + { + foreach (KeyValuePair updateHandler in _updateHandlers) + { + var result = updateHandler.Value.RemoveComponent(c); + result.data.ClearSubmitted(); + updateHandler.Key.handler(ToItem(result.data), DataFeedItemChange.Removed); + } + } + + private void Update() + { + foreach (KeyValuePair updateHandler in _updateHandlers) + { + updateHandler.Key.handler(null, DataFeedItemChange.PathItemsInvalidated); + } + } + + private void ProcessUpdate(SearchPhraseFeedUpdateHandler handler, ComponentData data) + { + bool flag = true; + if (!string.IsNullOrEmpty(handler.searchPhrase)) + { + List list = Pool.BorrowList(); + List list2 = Pool.BorrowList(); + List list3 = Pool.BorrowList(); + SearchQueryParser.Parse(handler.searchPhrase, list, list2, list3); + if (!data.MatchesSearchParameters(list, list2, list3)) + { + flag = false; + } + Pool.Return(ref list); + Pool.Return(ref list2); + Pool.Return(ref list3); + } + if (!flag) + { + if (data.Submitted) + { + data.ClearSubmitted(); + handler.handler(ToItem(data), DataFeedItemChange.Removed); + } + return; + } + data.MarkSubmitted(); + DataFeedItem item = ToItem(data); + handler.handler(item, DataFeedItemChange.Added); + } + + private void Subscribe(Slot s) + { + s.ComponentAdded += AddComponent; + s.ComponentRemoved += RemoveComponent; + } + + private void Unsubscribe(Slot s) + { + s.ComponentAdded -= AddComponent; + s.ComponentRemoved -= RemoveComponent; + } + + protected override void OnChanges() + { + base.OnChanges(); + if (!TargetSlot.WasChanged && !IncludeChildrenSlots.WasChanged) + { + return; + } + if (TargetSlot.WasChanged) + { + if (_lastSlot != null) + { + Unsubscribe(_lastSlot); + if (IncludeChildrenSlots) + { + _lastSlot.ForeachChild(childSlot => Unsubscribe(childSlot)); + } + } + if (TargetSlot.Target is Slot slot) + { + Subscribe(slot); + if (IncludeChildrenSlots) + { + TargetSlot.Target.ForeachChild(childSlot => Subscribe(childSlot)); + } + _lastSlot = slot; + } + else + { + _lastSlot = null; + } + } + if (IncludeChildrenSlots.WasChanged && TargetSlot.Target != null) + { + if (IncludeChildrenSlots.Value) + { + TargetSlot.Target.ForeachChild(childSlot => Subscribe(childSlot)); + } + else if (!IncludeChildrenSlots.Value) + { + TargetSlot.Target.ForeachChild(childSlot => Unsubscribe(childSlot)); + } + } + TargetSlot.WasChanged = false; + IncludeChildrenSlots.WasChanged = false; + Update(); + } + + protected override void OnPrepareDestroy() + { + base.OnPrepareDestroy(); + if (TargetSlot.Target != null) + { + Unsubscribe(TargetSlot.Target); + if (IncludeChildrenSlots) + { + TargetSlot.Target.ForeachChild(childSlot => Unsubscribe(childSlot)); + } + } + } + + public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) + { + if (path != null && path.Count > 0) + { + yield break; + } + if (groupKeys != null && groupKeys.Count > 0) + { + yield break; + } + if (TargetSlot.Target == null) + { + yield break; + } + + ComponentsDataFeedData componentDataFeedData = (ComponentsDataFeedData)viewData; + componentDataFeedData.Clear(); + searchPhrase = searchPhrase?.Trim(); + + var components = IncludeChildrenSlots ? TargetSlot.Target.GetComponentsInChildren() : TargetSlot.Target.GetComponents(); + foreach (Component allComponent in components) + { + // If local elements are written to synced fields it can cause exceptions and crashes + if (allComponent.IsLocalElement) continue; + componentDataFeedData.RegisterComponent(allComponent); + foreach (ISyncMember syncMember in allComponent.SyncMembers) + { + if (syncMember.IsLocalElement) continue; + componentDataFeedData.AddMember(syncMember); + } + } + + List optionalTerms = Pool.BorrowList(); + List requiredTerms = Pool.BorrowList(); + List excludedTerms = Pool.BorrowList(); + SearchQueryParser.Parse(searchPhrase, optionalTerms, requiredTerms, excludedTerms); + foreach (ComponentData componentData in componentDataFeedData.ComponentData) + { + if (componentData.MatchesSearchParameters(optionalTerms, requiredTerms, excludedTerms)) + { + componentData.MarkSubmitted(); + yield return ToItem(componentData); + } + } + Pool.Return(ref optionalTerms); + Pool.Return(ref requiredTerms); + Pool.Return(ref excludedTerms); + } + + private DataFeedItem ToItem(ComponentData data) + { + return new ComponentDataFeedItem(data); + } + + public void ListenToUpdates(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler, object viewData) + { + if ((path == null || path.Count <= 0) && (groupKeys == null || groupKeys.Count <= 0)) + { + var data = (ComponentsDataFeedData)viewData; + _updateHandlers.Add(new SearchPhraseFeedUpdateHandler(handler, searchPhrase?.Trim()), data); + } + } + + public void UnregisterListener(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler) + { + if ((path == null || path.Count <= 0) && (groupKeys == null || groupKeys.Count <= 0)) + { + _updateHandlers.Remove(new SearchPhraseFeedUpdateHandler(handler, searchPhrase?.Trim())); + } + } + + public LocaleString PathSegmentName(string segment, int depth) + { + return null; + } + + public object RegisterViewData() + { + return new ComponentsDataFeedData(); + } + + public void UnregisterViewData(object data) + { + } + + protected override void OnDispose() + { + _updateHandlers.Clear(); + base.OnDispose(); + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentsDataFeedData.cs b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentsDataFeedData.cs new file mode 100644 index 0000000..4d99757 --- /dev/null +++ b/ProjectObsidian/Components/Radiant UI/Data Feeds/Feeds/ComponentsDataFeedData.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System; +using FrooxEngine; + +namespace Obsidian; + +internal class ComponentsDataFeedData +{ + private List _data = new List(); + + private Dictionary _dataByUniqueId = new Dictionary(); + + public IEnumerable ComponentData => _data; + + public void Clear() + { + _data.Clear(); + _dataByUniqueId.Clear(); + } + + public ComponentData RegisterComponent(Component c) + { + bool created; + ComponentData componentData = EnsureEntry(c, out created); + + if (!created) + { + throw new InvalidOperationException("Component with this ReferenceID has already been added!"); + } + + componentData.component = c; + return componentData; + } + + private ComponentData RegisterMember(ISyncMember member, out bool createdEntry) + { + ComponentData componentData = EnsureEntry(member.FindNearestParent(), out createdEntry); + componentData.AddMember(member); + _dataByUniqueId[member.ReferenceID.ToString()] = componentData; + return componentData; + } + + public ComponentDataResult AddMember(ISyncMember member) + { + bool createdEntry; + return new ComponentDataResult(RegisterMember(member, out createdEntry), (!createdEntry) ? DataFeedItemChange.Updated : DataFeedItemChange.Added); + } + + public ComponentDataResult RemoveComponent(Component c) + { + if (!_dataByUniqueId.TryGetValue(c.ReferenceID.ToString(), out var value)) + { + return new ComponentDataResult(null, DataFeedItemChange.Unchanged); + } + _dataByUniqueId.Remove(c.ReferenceID.ToString()); + RemoveEntry(value); + return new ComponentDataResult(value, DataFeedItemChange.Removed); + } + + private void RemoveEntry(ComponentData data) + { + _data.Remove(data); + _dataByUniqueId.Remove(data.component.ReferenceID.ToString()); + } + + private ComponentData EnsureEntry(Component c, out bool created) + { + if (_dataByUniqueId.TryGetValue(c.ReferenceID.ToString(), out var value)) + { + created = false; + return value; + } + value = new ComponentData(c); + _data.Add(value); + _dataByUniqueId.Add(c.ReferenceID.ToString(), value); + created = true; + return value; + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Radiant UI/Data Feeds/Interfaces/ComponentDataItemInterface.cs b/ProjectObsidian/Components/Radiant UI/Data Feeds/Interfaces/ComponentDataItemInterface.cs new file mode 100644 index 0000000..d9d7692 --- /dev/null +++ b/ProjectObsidian/Components/Radiant UI/Data Feeds/Interfaces/ComponentDataItemInterface.cs @@ -0,0 +1,24 @@ +using FrooxEngine; + +namespace Obsidian; + +[Category(new string[] { "Obsidian/Radiant UI/Data Feeds/Interfaces" })] +public class ComponentDataItemInterface : FeedItemInterface +{ + public readonly SyncRef> Component; + + public readonly SyncRef> MemberCount; + + public readonly FeedSubTemplate, FeedEntityInterface> Members; + + public override void Set(IDataFeedView view, DataFeedItem item) + { + base.Set(view, item); + if (item is ComponentDataFeedItem componentDataFeedItem) + { + Component.TrySetTarget(componentDataFeedItem.Data.component); + MemberCount.TrySetTarget(componentDataFeedItem.Data.MemberCount); + Members.Set(componentDataFeedItem.Members, view); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProjectObsidian.csproj b/ProjectObsidian/ProjectObsidian.csproj index e397d44..544d88c 100644 --- a/ProjectObsidian/ProjectObsidian.csproj +++ b/ProjectObsidian/ProjectObsidian.csproj @@ -12,6 +12,7 @@ C:\Program Files (x86)\Steam\steamapps\common\Resonite\ $(HOME)/.steam/steam/steamapps/common/Resonite/ /mnt/LocalDisk2/SteamLibrary/steamapps/common/Resonite/ + G:\SteamLibrary\steamapps\common\Resonite\ true @@ -26,6 +27,9 @@ $(ResonitePath)Resonite_Data/Managed/FrooxEngine.dll + + $(ResonitePath)Resonite_Data/Managed/Microsoft.Bcl.AsyncInterfaces.dll + $(ResonitePath)Resonite_Data/Managed/Newtonsoft.Json.dll @@ -44,6 +48,9 @@ $(ResonitePath)Resonite_Data/Managed/Elements.Assets.dll + + $(ResonitePath)Resonite_Data/Managed/SkyFrost.Base.dll + $(ResonitePath)/Resonite_Data/Managed/SteamVR.dll @@ -53,4 +60,7 @@ + + +