diff --git a/workers/unity/Packages/io.improbable.gdk.core/EntityId.cs b/workers/unity/Packages/io.improbable.gdk.core/EntityId.cs index 50486cab0f..3ca5077d75 100644 --- a/workers/unity/Packages/io.improbable.gdk.core/EntityId.cs +++ b/workers/unity/Packages/io.improbable.gdk.core/EntityId.cs @@ -9,7 +9,7 @@ namespace Improbable.Gdk.Core /// Instances of this type should be treated as transient identifiers that will not be /// consistent between different runs of the same simulation. /// - public readonly struct EntityId : IEquatable + public readonly struct EntityId : IEquatable, IComparable, IComparable { /// /// The value of the EntityId. @@ -82,5 +82,42 @@ public override string ToString() { return Id.ToString(); } + + public int CompareTo(EntityId other) + { + return Id.CompareTo(other.Id); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + return obj is EntityId other + ? CompareTo(other) + : throw new ArgumentException($"Object must be of type {nameof(EntityId)}"); + } + + public static bool operator <(EntityId left, EntityId right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator >(EntityId left, EntityId right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator <=(EntityId left, EntityId right) + { + return left.CompareTo(right) <= 0; + } + + public static bool operator >=(EntityId left, EntityId right) + { + return left.CompareTo(right) >= 0; + } } } diff --git a/workers/unity/Packages/io.improbable.gdk.core/Systems/EntitySystem.cs b/workers/unity/Packages/io.improbable.gdk.core/Systems/EntitySystem.cs index a874512ae8..c30066c432 100644 --- a/workers/unity/Packages/io.improbable.gdk.core/Systems/EntitySystem.cs +++ b/workers/unity/Packages/io.improbable.gdk.core/Systems/EntitySystem.cs @@ -10,6 +10,8 @@ namespace Improbable.Gdk.Core [UpdateBefore(typeof(SpatialOSReceiveSystem))] public class EntitySystem : ComponentSystem { + public int ViewVersion { get; private set; } + private readonly List entitiesAdded = new List(); private readonly List entitiesRemoved = new List(); @@ -49,6 +51,11 @@ internal void ApplyDiff(ViewDiff diff) { entitiesRemoved.Add(entityId); } + + if (entitiesAdded.Count != 0 || entitiesRemoved.Count != 0) + { + ViewVersion += 1; + } } } diff --git a/workers/unity/Packages/io.improbable.gdk.debug/AssemblyInfo.cs b/workers/unity/Packages/io.improbable.gdk.debug/AssemblyInfo.cs new file mode 100644 index 0000000000..f99ce9eae1 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Improbable.Gdk.Debug.EditmodeTests")] diff --git a/workers/unity/Packages/io.improbable.gdk.debug/AssemblyInfo.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/AssemblyInfo.cs.meta new file mode 100644 index 0000000000..66c3abe316 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 72f220b785e04218af005b4af61d8b15 +timeCreated: 1590588224 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Improbable.Gdk.Debug.asmdef b/workers/unity/Packages/io.improbable.gdk.debug/Improbable.Gdk.Debug.asmdef index 1aa1367bf5..ccf24c2ea5 100644 --- a/workers/unity/Packages/io.improbable.gdk.debug/Improbable.Gdk.Debug.asmdef +++ b/workers/unity/Packages/io.improbable.gdk.debug/Improbable.Gdk.Debug.asmdef @@ -4,7 +4,8 @@ "Improbable.Gdk.Core", "Unity.Entities", "Sirenix.OdinInspector.Editor", - "Improbable.Gdk.Core.Editor" + "Improbable.Gdk.Core.Editor", + "Improbable.Gdk.Generated" ], "includePlatforms": [ "Editor" @@ -15,5 +16,6 @@ "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], - "versionDefines": [] + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests.meta b/workers/unity/Packages/io.improbable.gdk.debug/Tests.meta new file mode 100644 index 0000000000..da62255075 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 837a736a8799bd940bc76126787b6f01 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode.meta b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode.meta new file mode 100644 index 0000000000..c7d06ee6a6 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c329569152bc6554ebbb4382db63edee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/Improbable.Gdk.Debug.EditmodeTests.asmdef b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/Improbable.Gdk.Debug.EditmodeTests.asmdef new file mode 100644 index 0000000000..07df8e43de --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/Improbable.Gdk.Debug.EditmodeTests.asmdef @@ -0,0 +1,28 @@ +{ + "name": "Improbable.Gdk.Debug.EditmodeTests", + "references": [ + "GUID:40425aeb5a2b3f74797745fff21bc766", + "GUID:c5f2ea2f695256346af9ccd76c6f055d", + "GUID:edb3612c44ad0d24988d387581fd5fbe", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:734d92eba21c94caba915361bd5ac177", + "GUID:bd66e1825421d9041a8f2c4fa9ca562a" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Improbable.Worker.CInterop.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/Improbable.Gdk.Debug.EditmodeTests.asmdef.meta b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/Improbable.Gdk.Debug.EditmodeTests.asmdef.meta new file mode 100644 index 0000000000..93ace71d0b --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/Improbable.Gdk.Debug.EditmodeTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d78f101dce55d7245a4828958f3db5ec +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector.meta b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector.meta new file mode 100644 index 0000000000..a9461913a4 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 94d8459bdb6c42bd928f173eeb8c7a73 +timeCreated: 1590588188 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntityListDataTests.cs b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntityListDataTests.cs new file mode 100644 index 0000000000..3b150bfb76 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntityListDataTests.cs @@ -0,0 +1,146 @@ +using Improbable.Gdk.Core; +using Improbable.Gdk.Debug.WorkerInspector; +using Improbable.Gdk.TestUtils; +using NUnit.Framework; + +namespace Improbable.Gdk.Debug.EditmodeTests.WorkerInspector +{ + [TestFixture] + public class EntityListDataTests : MockBase + { + [TestCase] + public void Data_is_empty_intially() + { + var data = new EntityListData(); + Assert.IsEmpty(data.FilteredData); + } + + [TestCase] + public void SetNewWorld_should_not_throw() + { + var data = new EntityListData(); + Assert.DoesNotThrow(() => data.SetNewWorld(null)); + } + + [TestCase] + public void RefreshData_does_not_throw_if_no_world() + { + var data = new EntityListData(); + Assert.DoesNotThrow(() => data.RefreshData()); + } + + [TestCase] + public void ApplySearch_does_not_throw_if_no_world() + { + var data = new EntityListData(); + Assert.DoesNotThrow(() => data.ApplySearch(EntitySearchParameters.FromSearchString(""))); + } + + [TestCase] + public void RefreshData_finds_entities_after_setting_world() + { + World + .Step(world => + { + world.Connection.CreateEntity(1, GetTemplate("some-entity")); + }) + .Step(world => + { + var data = new EntityListData(); + data.SetNewWorld(world.Worker.World); // Yikes + return data; + }) + .Step((world, data) => + { + data.RefreshData(); + Assert.AreEqual(1, data.FilteredData.Count); + + var entityData = data.FilteredData[0]; + Assert.AreEqual(1, entityData.EntityId.Id); + Assert.AreEqual("some-entity", entityData.Metadata); + }); + } + + [TestCase] + public void ApplySearchFilter_immediately_filters_data() + { + World + .Step(world => + { + world.Connection.CreateEntity(1, GetTemplate("some-entity")); + }) + .Step(world => + { + var data = new EntityListData(); + data.SetNewWorld(world.Worker.World); // Yikes + return data; + }) + .Step((world, data) => + { + data.RefreshData(); + Assert.AreEqual(1, data.FilteredData.Count); + }) + .Step((world, data) => + { + data.ApplySearch(EntitySearchParameters.FromSearchString("2")); // Entity ID = 2 + Assert.IsEmpty(data.FilteredData); + }); + } + + [TestCase] + public void SetNewWorld_resets_data() + { + World + .Step(world => + { + world.Connection.CreateEntity(1, GetTemplate("some-entity")); + }) + .Step(world => + { + var data = new EntityListData(); + data.SetNewWorld(world.Worker.World); // Yikes + return data; + }) + .Step((world, data) => + { + data.RefreshData(); + Assert.AreEqual(1, data.FilteredData.Count); + }) + .Step((world, data) => + { + data.SetNewWorld(null); + Assert.IsEmpty(data.FilteredData); + }); + } + + [TestCase] + public void SearchFilter_persists_through_RefreshData() + { + World + .Step(world => + { + world.Connection.CreateEntity(1, GetTemplate("some-entity")); + }) + .Step(world => + { + var data = new EntityListData(); + data.ApplySearch(EntitySearchParameters.FromSearchString("2")); // Entity ID = 2 + data.SetNewWorld(world.Worker.World); // Yikes + return data; + }) + .Step((world, data) => + { + data.RefreshData(); + Assert.IsEmpty(data.FilteredData); + }); + } + + private EntityTemplate GetTemplate(string metadata) + { + var template = new EntityTemplate(); + template.AddComponent(new Position.Snapshot()); + template.AddComponent(new Metadata.Snapshot(metadata)); + return template; + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntityListDataTests.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntityListDataTests.cs.meta new file mode 100644 index 0000000000..bace140668 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntityListDataTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3a122e7880b745348c78c20fb3e3bf7e +timeCreated: 1590589452 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntitySearchParametersTests.cs b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntitySearchParametersTests.cs new file mode 100644 index 0000000000..e4ad9dc439 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntitySearchParametersTests.cs @@ -0,0 +1,48 @@ +using Improbable.Gdk.Debug.WorkerInspector; +using NUnit.Framework; + +namespace Improbable.Gdk.Debug.EditmodeTests.WorkerInspector +{ + [TestFixture] + public class EntitySearchParametersTests + { + [TestCase("1", 1)] + [TestCase(" 1 ", 1)] + [TestCase("1 ", 1)] + public void FromSearchString_parses_out_numbers(string searchString, long resultingId) + { + var searchParams = EntitySearchParameters.FromSearchString(searchString); + Assert.IsTrue(searchParams.EntityId.HasValue); + Assert.AreEqual(resultingId, searchParams.EntityId.Value.Id); + } + + [TestCase("0")] + [TestCase("-1")] + [TestCase("300 spartans")] + [TestCase("9223372036854775808")] // Max long + 1 + public void FromSearchString_rejects_non_entity_id_values(string searchString) + { + var searchParams = EntitySearchParameters.FromSearchString(searchString); + Assert.IsFalse(searchParams.EntityId.HasValue); + } + + [TestCase("some value", "some value")] + [TestCase(" with leading space", "with leading space")] + [TestCase("with trailing space ", "with trailing space")] + [TestCase(" with both! ", "with both!")] + public void FromSearchString_ignores_leading_and_trailing_whitespace(string searchString, string expectedFragment) + { + var searchParams = EntitySearchParameters.FromSearchString(searchString); + Assert.IsNotNull(searchParams.SearchFragment); + Assert.AreEqual(expectedFragment, searchParams.SearchFragment); + } + + [TestCase] + public void FromSearchString_converts_all_to_lower() + { + var searchParams = EntitySearchParameters.FromSearchString("CAPS"); + Assert.IsNotNull(searchParams.SearchFragment); + Assert.AreEqual("caps", searchParams.SearchFragment); + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntitySearchParametersTests.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntitySearchParametersTests.cs.meta new file mode 100644 index 0000000000..39c16770ff --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/Tests/Editmode/WorkerInspector/EntitySearchParametersTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4a9fc6bcd79d4ecfa90d11adf260fbfe +timeCreated: 1590588189 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector.meta new file mode 100644 index 0000000000..d26ffd5218 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fec542b6025e444e82e9889075a7de80 +timeCreated: 1590499259 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityList.cs b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityList.cs new file mode 100644 index 0000000000..70f2933bac --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityList.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using Improbable.Gdk.Core; +using Unity.Entities; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace Improbable.Gdk.Debug.WorkerInspector +{ + public class EntityList : VisualElement + { + public delegate void EntitySelected(EntityId entityId); + + public EntitySelected OnEntitySelected; + + private readonly EntityListData entities = new EntityListData(); + private readonly ListView listView; + + private EntitySystem entitySystem; + private int lastViewVersion; + private EntityData? selectedEntity; + + public EntityList() + { + const string uxmlPath = "Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml"; + var template = AssetDatabase.LoadAssetAtPath(uxmlPath); + template.CloneTree(this); + + listView = this.Q(); + listView.makeItem = () => new Label(); + listView.bindItem = BindItem; + listView.onSelectionChanged += OnSelectionChanged; + listView.itemsSource = entities.FilteredData; + + var searchField = this.Q(); + searchField.RegisterCallback>(OnSearchFieldChanged); + } + + public void Update() + { + if (entitySystem == null) + { + listView.Refresh(); + return; + } + + if (entitySystem.ViewVersion == lastViewVersion) + { + return; + } + + entities.RefreshData(); + listView.Refresh(); + + // Attempt to continue focusing the previously selected value. + if (selectedEntity != null) + { + var index = entities.FilteredData.IndexOf(selectedEntity.Value); + + if (index != -1) + { + listView.selectedIndex = index; + } + } + + lastViewVersion = entitySystem.ViewVersion; + } + + public void SetWorld(World world) + { + entitySystem = world?.GetExistingSystem(); + lastViewVersion = 0; + selectedEntity = null; + + entities.SetNewWorld(world); + } + + private void BindItem(VisualElement element, int index) + { + var entity = entities.FilteredData[index]; + var label = (Label) element; + label.text = entity.ToString(); + } + + private void OnSelectionChanged(List selections) + { + if (selections.Count != 1) + { + throw new InvalidOperationException("Unexpectedly selected more than one entity."); + } + + if (!(selections[0] is EntityData entityData)) + { + throw new InvalidOperationException($"Unexpected type for selection: {selections[0].GetType()}"); + } + + if (!selectedEntity.HasValue || selectedEntity.Value != entityData) + { + OnEntitySelected?.Invoke(entityData.EntityId); + selectedEntity = entityData; + } + } + + private void OnSearchFieldChanged(ChangeEvent changeEvent) + { + var searchValue = changeEvent.newValue.Trim(); + entities.ApplySearch(EntitySearchParameters.FromSearchString(searchValue)); + listView.Refresh(); + } + + public new class UxmlFactory : UxmlFactory + { + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityList.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityList.cs.meta new file mode 100644 index 0000000000..7b3dd4da3b --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74dd26db03e54251820f5b0d0d8a532f +timeCreated: 1590499801 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityListData.cs b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityListData.cs new file mode 100644 index 0000000000..fd9d234b4f --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityListData.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using Improbable.Gdk.Core; +using Unity.Collections; +using Unity.Entities; +using Unity.Profiling; + +namespace Improbable.Gdk.Debug.WorkerInspector +{ + internal class EntityListData + { + private static ProfilerMarker refreshDataMarker = new ProfilerMarker("EntityList.RefreshData"); + private static ProfilerMarker applySearchMarker = new ProfilerMarker("EntityList.ApplySearch"); + + public readonly List FilteredData = new List(); + + private readonly List fullData = new List(); + private World world; + private EntityQuery query; + private EntitySearchParameters searchParameters; + + public void ApplySearch(EntitySearchParameters searchParameters) + { + this.searchParameters = searchParameters; + + using (applySearchMarker.Auto()) + { + FilteredData.Clear(); + + foreach (var datum in fullData) + { + if (datum.Matches(searchParameters)) + { + FilteredData.Add(datum); + } + } + } + } + + public void SetNewWorld(World newWorld) + { + fullData.Clear(); + FilteredData.Clear(); + query?.Dispose(); + + if (newWorld == null) + { + world = null; + query = null; + return; + } + + // Need to refresh the query. + world = newWorld; + query = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); + } + + public void RefreshData() + { + if (world == null) + { + return; + } + + using (refreshDataMarker.Auto()) + { + fullData.Clear(); + var spatialOSComponentType = world.EntityManager.GetArchetypeChunkComponentType(true); + var metadataComponentType = + world.EntityManager.GetArchetypeChunkComponentType(true); + var ecsEntityType = world.EntityManager.GetArchetypeChunkEntityType(); + + using (var chunks = query.CreateArchetypeChunkArray(Allocator.TempJob)) + { + foreach (var chunk in chunks) + { + NativeArray? metadataArray = null; + + if (chunk.Has(metadataComponentType)) + { + metadataArray = chunk.GetNativeArray(metadataComponentType); + } + + var entityIdArray = chunk.GetNativeArray(spatialOSComponentType); + var entities = chunk.GetNativeArray(ecsEntityType); + + for (var i = 0; i < entities.Length; i++) + { + var data = new EntityData(entityIdArray[i].EntityId, metadataArray?[i].EntityType); + fullData.Add(data); + } + } + } + + fullData.Sort(); + } + + ApplySearch(searchParameters); + } + } + + internal readonly struct EntityData : IComparable, IComparable, IEquatable + { + public readonly EntityId EntityId; + public readonly string Metadata; + + public EntityData(EntityId entityId, string metadata) + { + EntityId = entityId; + Metadata = metadata; + } + + public bool Matches(EntitySearchParameters searchParameters) + { + if (searchParameters.EntityId.HasValue) + { + return searchParameters.EntityId.Value == EntityId; + } + + if (!string.IsNullOrEmpty(searchParameters.SearchFragment)) + { + return Metadata.ToLower().Contains(searchParameters.SearchFragment); + } + + return true; + } + + public override string ToString() + { + return Metadata != null ? $"{Metadata} ({EntityId})" : EntityId.ToString(); + } + + public int CompareTo(EntityData other) + { + return EntityId.CompareTo(other.EntityId); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + return obj is EntityData other + ? CompareTo(other) + : throw new ArgumentException($"Object must be of type {nameof(EntityData)}"); + } + + public bool Equals(EntityData other) + { + return EntityId.Equals(other.EntityId) && Metadata == other.Metadata; + } + + public override bool Equals(object obj) + { + return obj is EntityData other && Equals(other); + } + + public static bool operator ==(EntityData left, EntityData right) + { + return left.Equals(right); + } + + public static bool operator !=(EntityData left, EntityData right) + { + return !left.Equals(right); + } + + public override int GetHashCode() + { + unchecked + { + return (EntityId.GetHashCode() * 397) ^ (Metadata != null ? Metadata.GetHashCode() : 0); + } + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityListData.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityListData.cs.meta new file mode 100644 index 0000000000..5d91586663 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntityListData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9272d2e833dd417db4e7d5ed84d41721 +timeCreated: 1590584740 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntitySearchParameters.cs b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntitySearchParameters.cs new file mode 100644 index 0000000000..62c347660c --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntitySearchParameters.cs @@ -0,0 +1,33 @@ +using Improbable.Gdk.Core; + +namespace Improbable.Gdk.Debug.WorkerInspector +{ + internal readonly struct EntitySearchParameters + { + public readonly EntityId? EntityId; + public readonly string SearchFragment; + + private EntitySearchParameters(EntityId entityId) + { + EntityId = entityId; + SearchFragment = null; + } + + private EntitySearchParameters(string stringFragment) + { + EntityId = null; + SearchFragment = stringFragment; + } + + public static EntitySearchParameters FromSearchString(string searchValue) + { + // EntityID values are strictly positive + if (long.TryParse(searchValue, out var value) && value > 0) + { + return new EntitySearchParameters(new EntityId(value)); + } + + return new EntitySearchParameters(searchValue.Trim().ToLower()); + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntitySearchParameters.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntitySearchParameters.cs.meta new file mode 100644 index 0000000000..718b73f0ae --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/EntitySearchParameters.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2713cbce33a74c118ace3eb1ba1319eb +timeCreated: 1590584835 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates.meta new file mode 100644 index 0000000000..751951c97b --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c925b523d8ad4eb18c214dd28d89b333 +timeCreated: 1590499485 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml new file mode 100644 index 0000000000..abec4e955c --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml.meta new file mode 100644 index 0000000000..2e71ec8622 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/EntityList.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 026ee74d641c4fafa50d4dee5143a44c +timeCreated: 1590499931 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss new file mode 100644 index 0000000000..b324ba3566 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss @@ -0,0 +1,26 @@ +.entity-list-view { + flex-grow: 1; + flex-direction: column; + --unity-item-height: 24; +} + +.unity-list-view__item { + padding-left: 8px; + -unity-text-align: middle-left; +} + +.entity-panel { + flex-grow: 1; + flex-direction: column; + max-width: 33%; + background-color: rgba(0, 0, 0, 0.05); + border-right-color: rgba(0, 0, 0, 0.2); + border-right-width: 1px; + border-top-color: rgba(0, 0, 0, 0.2); + border-top-width: 1px; +} + +.entity-list-searchbar { + flex-grow: 1; + width: auto; +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss.meta new file mode 100644 index 0000000000..208e2b605c --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e85590b77245430ebe322aab14fdd04a +timeCreated: 1590503323 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml new file mode 100644 index 0000000000..a1c8bf6e80 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml @@ -0,0 +1,8 @@ + + + + diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml.meta new file mode 100644 index 0000000000..1f8895792b --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f4791bd1d0764905a706bede3e487305 +timeCreated: 1590499498 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/WorkerInspectorWindow.cs b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/WorkerInspectorWindow.cs new file mode 100644 index 0000000000..98ee6ef764 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/WorkerInspectorWindow.cs @@ -0,0 +1,62 @@ +using Improbable.Gdk.Core; +using Improbable.Gdk.Core.Editor.UIElements; +using Unity.Entities; +using UnityEditor; +using UnityEngine.UIElements; + +namespace Improbable.Gdk.Debug.WorkerInspector +{ + internal class WorkerInspectorWindow : EditorWindow + { + private WorldSelector worldSelector; + private EntityList entityList; + + [MenuItem("SpatialOS/Window/Worker Inspector", false)] + public static void ShowWindow() + { + var windowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.InspectorWindow"); + var window = GetWindow("Worker Inspector", windowType); + window.Show(); + } + + private void OnEnable() + { + SetupUI(); + worldSelector.UpdateWorldSelection(); + } + + private void OnInspectorUpdate() + { + worldSelector.UpdateWorldSelection(); + entityList.Update(); + } + + private void SetupUI() + { + const string windowUxmlPath = "Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uxml"; + const string windowUssPath = "Packages/io.improbable.gdk.debug/WorkerInspector/Templates/WorkerInspectorWindow.uss"; + + var windowTemplate = AssetDatabase.LoadAssetAtPath(windowUxmlPath); + windowTemplate.CloneTree(rootVisualElement); + + var stylesheet = AssetDatabase.LoadAssetAtPath(windowUssPath); + rootVisualElement.styleSheets.Add(stylesheet); + + worldSelector = rootVisualElement.Q(); + worldSelector.OnWorldChanged += OnWorldChanged; + + entityList = rootVisualElement.Q(); + entityList.OnEntitySelected += OnEntitySelected; + } + + private void OnWorldChanged(World world) + { + entityList.SetWorld(world); + } + + private void OnEntitySelected(EntityId entityId) + { + UnityEngine.Debug.Log($"Selected {entityId}"); + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/WorkerInspectorWindow.cs.meta b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/WorkerInspectorWindow.cs.meta new file mode 100644 index 0000000000..ce602cce98 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.debug/WorkerInspector/WorkerInspectorWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d2a648c47d514935b7f10da5942442e3 +timeCreated: 1590499271 \ No newline at end of file