diff --git a/AutoSceneChanger.cs b/AutoSceneChanger.cs new file mode 100644 index 0000000..c7b60ea --- /dev/null +++ b/AutoSceneChanger.cs @@ -0,0 +1,13 @@ +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace NEG.Utils +{ + public class AutoSceneChanger : MonoBehaviour + { +#if !UNITY_EDITOR + private void Start() => SceneManager.LoadScene(1); +#endif + } +} + diff --git a/AutoSceneChanger.cs.meta b/AutoSceneChanger.cs.meta new file mode 100644 index 0000000..c7db448 --- /dev/null +++ b/AutoSceneChanger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eac2f9a681087504998bbeb40cd8516a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Collections.meta b/Collections.meta new file mode 100644 index 0000000..d087cf9 --- /dev/null +++ b/Collections.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9b25b74f42726e94585fe6ae4b9dd947 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Collections/DictionaryExtensions.cs b/Collections/DictionaryExtensions.cs new file mode 100644 index 0000000..aae8985 --- /dev/null +++ b/Collections/DictionaryExtensions.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace NEG.Utils.Collections +{ + public static class DictionaryExtensions + { + /// + /// Adds given value to a dictionary if there was no element at given , replaces element with otherwise. + /// + /// true if element was added, false if it was replaced + public static bool AddOrUpdate(this Dictionary dict, K key, V value) + { + if (dict.ContainsKey(key)) + { + dict[key] = value; + return false; + } + else + { + dict.Add(key, value); + return true; + } + } + + + /// + /// Gets a value from the dictionary under a specified key or adds it if did not exist and returns . + /// + /// value under a given if it exists, otherwise + public static V GetOrSetToDefault(this Dictionary dict, K key, V defaultValue) + { + if (dict.TryGetValue(key, out V value)) + { + return value; + } + dict.Add(key, defaultValue); + + return defaultValue; + } + } +} diff --git a/Collections/DictionaryExtensions.cs.meta b/Collections/DictionaryExtensions.cs.meta new file mode 100644 index 0000000..d6cb872 --- /dev/null +++ b/Collections/DictionaryExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a2c99d7a9a2f8184f8b8e94c01723ec6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ComponentsAdditionalItems.meta b/Editor/ComponentsAdditionalItems.meta new file mode 100644 index 0000000..3129799 --- /dev/null +++ b/Editor/ComponentsAdditionalItems.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d683d64cc04efcf479b41c84b51cf7fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ComponentsAdditionalItems/CanvasScalerDefault.cs b/Editor/ComponentsAdditionalItems/CanvasScalerDefault.cs new file mode 100644 index 0000000..a6cb782 --- /dev/null +++ b/Editor/ComponentsAdditionalItems/CanvasScalerDefault.cs @@ -0,0 +1,30 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +namespace NEG.Utils.Editor.ComponentsAdditionalItems +{ + public static class CanvasScalerDefault + { + [MenuItem("CONTEXT/CanvasScaler/Full HD horizontal", false, 2000)] + public static void SetFullHdHorizontal(MenuCommand command) => SetComponent(command, 1920, 1080); + [MenuItem("CONTEXT/CanvasScaler/Full HD vertical", false, 2000)] + public static void SetFullHdVertical(MenuCommand command) => SetComponent(command, 1080, 1920); + [MenuItem("CONTEXT/CanvasScaler/Full 2k horizontal", false, 2000)] + public static void Set2KHorizontal(MenuCommand command) => SetComponent(command, 2560, 1440 ); + [MenuItem("CONTEXT/CanvasScaler/Full 2k vertical", false, 2000)] + public static void Set2KVertical(MenuCommand command) => SetComponent(command, 1440, 2560); + [MenuItem("CONTEXT/CanvasScaler/Full 4k horizontal", false, 2000)] + public static void Set4KHorizontal(MenuCommand command) => SetComponent(command, 3840, 2160); + [MenuItem("CONTEXT/CanvasScaler/Full 4k vertical", false, 2000)] + public static void Set4KVertical(MenuCommand command) => SetComponent(command, 2160, 3840); + + private static void SetComponent(MenuCommand command, int width, int height) + { + var scaler = (CanvasScaler)command.context; + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.matchWidthOrHeight = width > height ? 1f : 0f; + scaler.referenceResolution = new Vector2(width, height); + } + } +} \ No newline at end of file diff --git a/Editor/ComponentsAdditionalItems/CanvasScalerDefault.cs.meta b/Editor/ComponentsAdditionalItems/CanvasScalerDefault.cs.meta new file mode 100644 index 0000000..781e85f --- /dev/null +++ b/Editor/ComponentsAdditionalItems/CanvasScalerDefault.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7ddebddb1a1c1947be05ac9e96aceb8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ComponentsAdditionalItems/RectTransformSetBasedOnImage.cs b/Editor/ComponentsAdditionalItems/RectTransformSetBasedOnImage.cs new file mode 100644 index 0000000..f1f1691 --- /dev/null +++ b/Editor/ComponentsAdditionalItems/RectTransformSetBasedOnImage.cs @@ -0,0 +1,22 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +namespace NEG.Utils.Editor.ComponentsAdditionalItems +{ + public static class RectTransformSetBasedOnImage + { + [MenuItem("CONTEXT/RectTransform/Set as inside of Image", false, 2000)] + public static void SetFillBasedOnImage(MenuCommand command) + { + var transform = (RectTransform)command.context; + if (!transform.TryGetComponent(out Image image)) + return; + + transform.anchorMin = Vector2.zero; + transform.anchorMax = Vector2.one; + transform.offsetMin = new Vector2(-image.sprite.border.x, -image.sprite.border.y); + transform.offsetMax = new Vector2(image.sprite.border.z, image.sprite.border.w); + } + } +} \ No newline at end of file diff --git a/Editor/ComponentsAdditionalItems/RectTransformSetBasedOnImage.cs.meta b/Editor/ComponentsAdditionalItems/RectTransformSetBasedOnImage.cs.meta new file mode 100644 index 0000000..b1a832b --- /dev/null +++ b/Editor/ComponentsAdditionalItems/RectTransformSetBasedOnImage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 614a58a9665e7cf4182834f1fb3c0096 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/NEG.Utils.Editor.asmdef b/Editor/NEG.Utils.Editor.asmdef new file mode 100644 index 0000000..60746f0 --- /dev/null +++ b/Editor/NEG.Utils.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "NEG.Utils.Editor", + "rootNamespace": "", + "references": [ + "GUID:3c4294719a93e3c4e831a9ff0c261e8a" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/NEG.Utils.Editor.asmdef.meta b/Editor/NEG.Utils.Editor.asmdef.meta new file mode 100644 index 0000000..e1a6a84 --- /dev/null +++ b/Editor/NEG.Utils.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5928dc8d9173fd348aa77d4593ca3fd8 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ToolsWindowBase.cs b/Editor/ToolsWindowBase.cs new file mode 100644 index 0000000..d200732 --- /dev/null +++ b/Editor/ToolsWindowBase.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace NegUtils.Editor +{ + [InitializeOnLoad] + public class ToolsWindowBase : EditorWindow + { + static ToolsWindowBase() + { + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + } + + [MenuItem("Tools/Show Tools Window")] + private static void ShowWindow() + { + var window = GetWindow(); + window.Show(); + } + + protected virtual void OnGUI() + { + if (GUILayout.Button("Select Scene")) + ShowScenesList(GUILayoutUtility.GetLastRect()); + + bool startFromSceneIndex0 = EditorPrefs.GetBool("StartFromSceneIndex0"); + bool newVal = GUILayout.Toggle(startFromSceneIndex0, "Start from scene with index 0 on start"); + if (newVal != startFromSceneIndex0) + { + EditorPrefs.SetBool("StartFromSceneIndex0", newVal); + } + + if (startFromSceneIndex0) + { + bool goToCurrentScene = EditorPrefs.GetBool("GoToCurrentSceneAfterPlay"); + newVal = GUILayout.Toggle(goToCurrentScene, "Go to current scene after play"); + if (newVal != goToCurrentScene) + { + EditorPrefs.SetBool("GoToCurrentSceneAfterPlay", newVal); + } + } + } + + private static void ShowScenesList(Rect position) + { + var menu = new GenericMenu(); + + string path = Application.dataPath + "/Scenes/Production"; + string[] fileInfo = Directory.GetFiles(path, "*.unity"); + + foreach (string item in fileInfo) + { + string s = item; + menu.AddItem(new GUIContent(s.Remove(0, path.Length + 1).Remove(s.Length - path.Length - 7 ,6)), false, () => { + LoadScene(s); + }); + menu.AddSeparator(""); + } + menu.DropDown(position); + } + + private static void LoadScene(string path) + { + EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); + EditorSceneManager.OpenScene(path, OpenSceneMode.Single); + } + + private static void OnPlayModeStateChanged(PlayModeStateChange state) + { + switch(state) + { + case PlayModeStateChange.ExitingEditMode: + { + if(!EditorPrefs.GetBool("StartFromSceneIndex0")) + return; + EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); + EditorPrefs.SetString("LastOpenedScenePath", EditorSceneManager.GetSceneManagerSetup()[0].path); + EditorSceneManager.OpenScene(EditorBuildSettings.scenes[0].path); + } + break; + case PlayModeStateChange.EnteredPlayMode: + { + if(!EditorPrefs.GetBool("StartFromSceneIndex0")) + return; + + if (EditorPrefs.GetBool("GoToCurrentSceneAfterPlay")) + EditorSceneManager.LoadSceneInPlayMode(EditorPrefs.GetString("LastOpenedScenePath"), + new LoadSceneParameters(LoadSceneMode.Single)); + else + SceneManager.LoadScene(1); + } + break; + case PlayModeStateChange.EnteredEditMode: + { + if(!EditorPrefs.GetBool("StartFromSceneIndex0")) + return; + EditorSceneManager.OpenScene(EditorPrefs.GetString("LastOpenedScenePath")); + } + break; + } + + } + } +} \ No newline at end of file diff --git a/Editor/ToolsWindowBase.cs.meta b/Editor/ToolsWindowBase.cs.meta new file mode 100644 index 0000000..407b91d --- /dev/null +++ b/Editor/ToolsWindowBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 76faf7fadf7c40d88dec1a8ee4da5cb6 +timeCreated: 1671500708 \ No newline at end of file diff --git a/NEG.meta b/NEG.meta new file mode 100644 index 0000000..9b90a19 --- /dev/null +++ b/NEG.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 61eeed9106e147d8a76d7f0140d3571d +timeCreated: 1670708950 \ No newline at end of file diff --git a/NEG/UI.meta b/NEG/UI.meta new file mode 100644 index 0000000..3557a60 --- /dev/null +++ b/NEG/UI.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bbdebf147b9a40b0bc94b6f710c7aa6b +timeCreated: 1670690396 \ No newline at end of file diff --git a/NEG/UI/Area.meta b/NEG/UI/Area.meta new file mode 100644 index 0000000..605a7f9 --- /dev/null +++ b/NEG/UI/Area.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 853f6a7d0f224278afbb5457d0fb8bde +timeCreated: 1670706939 \ No newline at end of file diff --git a/NEG/UI/Area/IArea.cs b/NEG/UI/Area/IArea.cs new file mode 100644 index 0000000..4c84e27 --- /dev/null +++ b/NEG/UI/Area/IArea.cs @@ -0,0 +1,9 @@ +using NEG.UI.WindowSlot; + +namespace NEG.UI.Area +{ + public interface IArea : ISlotsHolder, IUiElement + { + + } +} \ No newline at end of file diff --git a/NEG/UI/Area/IArea.cs.meta b/NEG/UI/Area/IArea.cs.meta new file mode 100644 index 0000000..d37d498 --- /dev/null +++ b/NEG/UI/Area/IArea.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f7cf5ef3a347e1c4b98411f4d564b988 +timeCreated: 1670690282 \ No newline at end of file diff --git a/NEG/UI/IUiElement.cs b/NEG/UI/IUiElement.cs new file mode 100644 index 0000000..6cfde2b --- /dev/null +++ b/NEG/UI/IUiElement.cs @@ -0,0 +1,11 @@ +namespace NEG.UI +{ + public interface IUiElement + { + /// + /// Sets only visible state of element + /// + /// + void SetEnabled(bool setEnabled); + } +} \ No newline at end of file diff --git a/NEG/UI/IUiElement.cs.meta b/NEG/UI/IUiElement.cs.meta new file mode 100644 index 0000000..765fb83 --- /dev/null +++ b/NEG/UI/IUiElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 296bf6969a6347f8aea788a7bdd086af +timeCreated: 1670693177 \ No newline at end of file diff --git a/NEG/UI/NEG.UI.asmdef b/NEG/UI/NEG.UI.asmdef new file mode 100644 index 0000000..db95a9f --- /dev/null +++ b/NEG/UI/NEG.UI.asmdef @@ -0,0 +1,14 @@ +{ + "name": "NEG.UI", + "rootNamespace": "", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/NEG/UI/NEG.UI.asmdef.meta b/NEG/UI/NEG.UI.asmdef.meta new file mode 100644 index 0000000..b5859a4 --- /dev/null +++ b/NEG/UI/NEG.UI.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7361f1d9c43da6649923760766194746 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NEG/UI/Popup.meta b/NEG/UI/Popup.meta new file mode 100644 index 0000000..0b36d76 --- /dev/null +++ b/NEG/UI/Popup.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 76fc21263637443ca8268859d4cb5378 +timeCreated: 1670707809 \ No newline at end of file diff --git a/NEG/UI/Popup/DefaultPopupData.cs b/NEG/UI/Popup/DefaultPopupData.cs new file mode 100644 index 0000000..bcbbefb --- /dev/null +++ b/NEG/UI/Popup/DefaultPopupData.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace NEG.UI.Popup +{ + public class DefaultPopupData : PopupData + { + private readonly IDefaultPopup defaultPopup; + + private readonly string title; + private readonly string content; + private readonly List<(string, Action)> options; + + public DefaultPopupData(IDefaultPopup popup, string title, string content, List<(string, Action)> options) : base(popup) + { + defaultPopup = popup; + this.title = title; + this.content = content; + this.options = options; + } + + public override void Show() + { + defaultPopup.SetContent(title, content, options); + base.Show(); + } + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/DefaultPopupData.cs.meta b/NEG/UI/Popup/DefaultPopupData.cs.meta new file mode 100644 index 0000000..e86aa41 --- /dev/null +++ b/NEG/UI/Popup/DefaultPopupData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 372c6df9ec044cb3adc86c7776b2ef61 +timeCreated: 1672432934 \ No newline at end of file diff --git a/NEG/UI/Popup/IDefaultPopup.cs b/NEG/UI/Popup/IDefaultPopup.cs new file mode 100644 index 0000000..6a9216d --- /dev/null +++ b/NEG/UI/Popup/IDefaultPopup.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace NEG.UI.Popup +{ + public interface IDefaultPopup : IPopup + { + /// + /// Sets content based on provided data. + /// + /// popup title + /// popup content + /// list of tuples (name, action on click), to set buttons. Do not pass here popup closing logic, implementing class should do it + public void SetContent(string title, string content, List<(string name, Action action)> options); + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/IDefaultPopup.cs.meta b/NEG/UI/Popup/IDefaultPopup.cs.meta new file mode 100644 index 0000000..e510e78 --- /dev/null +++ b/NEG/UI/Popup/IDefaultPopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0a6d0871ada44d1d95bea6c8e8701769 +timeCreated: 1672153906 \ No newline at end of file diff --git a/NEG/UI/Popup/IPopup.cs b/NEG/UI/Popup/IPopup.cs new file mode 100644 index 0000000..e3468c7 --- /dev/null +++ b/NEG/UI/Popup/IPopup.cs @@ -0,0 +1,26 @@ +using JetBrains.Annotations; +using System; + +namespace NEG.UI.Popup +{ + [PublicAPI] + public interface IPopup + { + /// + /// Event to fire when popup is closed + /// + event Action OnPopupClosed; + + /// + /// Show popup + /// + /// data assigned to popup, used to give info that popup is closed + public void Show(PopupData data); + + /// + /// Close popup or mark as closed if not visible + /// + /// if true hide visually, without firing callbacks to properly close + void Close(bool silent = false); + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/IPopup.cs.meta b/NEG/UI/Popup/IPopup.cs.meta new file mode 100644 index 0000000..54795d7 --- /dev/null +++ b/NEG/UI/Popup/IPopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 79767831e4324605974f3bb0bb5026fb +timeCreated: 1670692499 \ No newline at end of file diff --git a/NEG/UI/Popup/PopupData.cs b/NEG/UI/Popup/PopupData.cs new file mode 100644 index 0000000..6bf528a --- /dev/null +++ b/NEG/UI/Popup/PopupData.cs @@ -0,0 +1,50 @@ +using JetBrains.Annotations; +using System; + +namespace NEG.UI.Popup +{ + [PublicAPI] + public class PopupData + { + /// + /// Event that is fired on closing popup. + /// + public event Action PopupClosedEvent + { + add => popup.OnPopupClosed += value; + remove => popup.OnPopupClosed -= value; + } + + /// + /// Is this data is still valid. If set to false, popup will not show. + /// + public bool IsValid { get; protected set; } + + private readonly IPopup popup; + + /// + /// PopupData constructor. + /// + /// attached to this data, can be used by different data instances + public PopupData(IPopup popup) + { + this.popup = popup; + IsValid = true; + } + + /// + /// Show popup and pass needed data. + /// + public virtual void Show() => popup.Show(this); + + /// + /// Hide popup. Close visuals without firing events; + /// + public virtual void Hide() => popup.Close(true); + + /// + /// Invalidate popup, will automatically skip this popup + /// + public virtual void Invalidate() => IsValid = false; + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/PopupData.cs.meta b/NEG/UI/Popup/PopupData.cs.meta new file mode 100644 index 0000000..2148759 --- /dev/null +++ b/NEG/UI/Popup/PopupData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6e5ebb367aa4dfba5e5f853c9b31a3d +timeCreated: 1672430446 \ No newline at end of file diff --git a/NEG/UI/PriorityQueue.cs b/NEG/UI/PriorityQueue.cs new file mode 100644 index 0000000..74e3e1b --- /dev/null +++ b/NEG/UI/PriorityQueue.cs @@ -0,0 +1,999 @@ +#nullable enable +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic +{ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ported from: +// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs + internal sealed class PriorityQueueDebugView + { + private readonly PriorityQueue _queue; + private readonly bool _sort; + + public PriorityQueueDebugView(PriorityQueue queue) + { + ArgumentNullException.ThrowIfNull(queue); + + _queue = queue; + _sort = true; + } + + public PriorityQueueDebugView(PriorityQueue.UnorderedItemsCollection collection) + { + _queue = collection?._queue ?? throw new System.ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public (TElement Element, TPriority Priority)[] Items + { + get + { + List<(TElement Element, TPriority Priority)> list = new(_queue.UnorderedItems); + if (_sort) list.Sort((i1, i2) => _queue.Comparer.Compare(i1.Priority, i2.Priority)); + + return list.ToArray(); + } + } + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class SR + { + internal const string ArgumentOutOfRange_NeedNonNegNum = "Non-negative number required."; + internal const string ArgumentOutOfRange_IndexMustBeLessOrEqual = "Index must be less or equal"; + internal const string InvalidOperation_EmptyQueue = "The queue is empty."; + internal const string InvalidOperation_EnumFailedVersion = "Collection modified while iterating over it."; + internal const string Arg_NonZeroLowerBound = "Non-zero lower bound required."; + internal const string Arg_RankMultiDimNotSupported = "Multi-dimensional arrays not supported."; + internal const string Argument_InvalidArrayType = "Invalid array type."; + internal const string Argument_InvalidOffLen = "Invalid offset or length."; + } + + internal static class ArgumentNullException + { + public static void ThrowIfNull(object o) + { + if (o == null) + throw new System.ArgumentNullException(); // hard to do it differently without C# 10's features + } + } + + internal static class ArrayEx + { + internal const int MaxLength = int.MaxValue; + } + + /// + /// Internal helper functions for working with enumerables. + /// + internal static class EnumerableHelpers + { + /// Converts an enumerable to an array using the same logic as List{T}. + /// The enumerable to convert. + /// The number of items stored in the resulting array, 0-indexed. + /// + /// The resulting array. The length of the array may be greater than , + /// which is the actual number of elements in the array. + /// + internal static T[] ToArray(IEnumerable source, out int length) + { + if (source is ICollection ic) + { + int count = ic.Count; + if (count != 0) + { + // Allocate an array of the desired size, then copy the elements into it. Note that this has the same + // issue regarding concurrency as other existing collections like List. If the collection size + // concurrently changes between the array allocation and the CopyTo, we could end up either getting an + // exception from overrunning the array (if the size went up) or we could end up not filling as many + // items as 'count' suggests (if the size went down). This is only an issue for concurrent collections + // that implement ICollection, which as of .NET 4.6 is just ConcurrentDictionary. + var arr = new T[count]; + ic.CopyTo(arr, 0); + length = count; + return arr; + } + } + else + { + using (var en = source.GetEnumerator()) + { + if (en.MoveNext()) + { + const int DefaultCapacity = 4; + var arr = new T[DefaultCapacity]; + arr[0] = en.Current; + int count = 1; + + while (en.MoveNext()) + { + if (count == arr.Length) + { + // This is the same growth logic as in List: + // If the array is currently empty, we make it a default size. Otherwise, we attempt to + // double the size of the array. Doubling will overflow once the size of the array reaches + // 2^30, since doubling to 2^31 is 1 larger than Int32.MaxValue. In that case, we instead + // constrain the length to be Array.MaxLength (this overflow check works because of the + // cast to uint). + int newLength = count << 1; + if ((uint)newLength > ArrayEx.MaxLength) + newLength = ArrayEx.MaxLength <= count ? count + 1 : ArrayEx.MaxLength; + + Array.Resize(ref arr, newLength); + } + + arr[count++] = en.Current; + } + + length = count; + return arr; + } + } + } + + length = 0; + return Array.Empty(); + } + } + + /// + /// Represents a min priority queue. + /// + /// Specifies the type of elements in the queue. + /// Specifies the type of priority associated with enqueued elements. + /// + /// Implements an array-backed quaternary min-heap. Each element is enqueued with an associated priority + /// that determines the dequeue order: elements with the lowest priority get dequeued first. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(PriorityQueueDebugView<,>))] + public class PriorityQueue + { + /// + /// Specifies the arity of the d-ary heap, which here is quaternary. + /// It is assumed that this value is a power of 2. + /// + private const int Arity = 4; + + /// + /// The binary logarithm of . + /// + private const int Log2Arity = 2; + + /// + /// Custom comparer used to order the heap. + /// + private readonly IComparer? _comparer; + + /// + /// Represents an implicit heap-ordered complete d-ary tree, stored as an array. + /// + private (TElement Element, TPriority Priority)[] _nodes; + + /// + /// The number of nodes in the heap. + /// + private int _size; + + /// + /// Lazily-initialized collection used to expose the contents of the queue. + /// + private UnorderedItemsCollection? _unorderedItems; + + /// + /// Version updated on mutation to help validate enumerators operate on a consistent state. + /// + private int _version; + +#if DEBUG + static PriorityQueue() + { + Debug.Assert(Log2Arity > 0 && Math.Pow(2, Log2Arity) == Arity); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + public PriorityQueue() + { + _nodes = Array.Empty<(TElement, TPriority)>(); + _comparer = InitializeComparer(null); + } + + /// + /// Initializes a new instance of the class + /// with the specified initial capacity. + /// + /// Initial capacity to allocate in the underlying heap array. + /// + /// The specified was negative. + /// + public PriorityQueue(int initialCapacity) + : this(initialCapacity, null) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified custom priority comparer. + /// + /// + /// Custom comparer dictating the ordering of elements. + /// Uses if the argument is . + /// + public PriorityQueue(IComparer? comparer) + { + _nodes = Array.Empty<(TElement, TPriority)>(); + _comparer = InitializeComparer(comparer); + } + + /// + /// Initializes a new instance of the class + /// with the specified initial capacity and custom priority comparer. + /// + /// Initial capacity to allocate in the underlying heap array. + /// + /// Custom comparer dictating the ordering of elements. + /// Uses if the argument is . + /// + /// + /// The specified was negative. + /// + public PriorityQueue(int initialCapacity, IComparer? comparer) + { + if (initialCapacity < 0) + throw new ArgumentOutOfRangeException( + nameof(initialCapacity), initialCapacity, SR.ArgumentOutOfRange_NeedNonNegNum); + + _nodes = new (TElement, TPriority)[initialCapacity]; + _comparer = InitializeComparer(comparer); + } + + /// + /// Initializes a new instance of the class + /// that is populated with the specified elements and priorities. + /// + /// The pairs of elements and priorities with which to populate the queue. + /// + /// The specified argument was . + /// + /// + /// Constructs the heap using a heapify operation, + /// which is generally faster than enqueuing individual elements sequentially. + /// + public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> items) + : this(items, null) + { + } + + /// + /// Initializes a new instance of the class + /// that is populated with the specified elements and priorities, + /// and with the specified custom priority comparer. + /// + /// The pairs of elements and priorities with which to populate the queue. + /// + /// Custom comparer dictating the ordering of elements. + /// Uses if the argument is . + /// + /// + /// The specified argument was . + /// + /// + /// Constructs the heap using a heapify operation, + /// which is generally faster than enqueuing individual elements sequentially. + /// + public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> items, IComparer? comparer) + { + ArgumentNullException.ThrowIfNull(items); + + _nodes = EnumerableHelpers.ToArray(items, out _size); + _comparer = InitializeComparer(comparer); + + if (_size > 1) Heapify(); + } + + /// + /// Gets the number of elements contained in the . + /// + public int Count => _size; + + /// + /// Gets the priority comparer used by the . + /// + public IComparer Comparer => _comparer ?? Comparer.Default; + + /// + /// Gets a collection that enumerates the elements of the queue in an unordered manner. + /// + /// + /// The enumeration does not order items by priority, since that would require N * log(N) time and N space. + /// Items are instead enumerated following the internal array heap layout. + /// + public UnorderedItemsCollection UnorderedItems => _unorderedItems ??= new UnorderedItemsCollection(this); + + /// + /// Adds the specified element with associated priority to the . + /// + /// The element to add to the . + /// The priority with which to associate the new element. + public void Enqueue(TElement element, TPriority priority) + { + // Virtually add the node at the end of the underlying array. + // Note that the node being enqueued does not need to be physically placed + // there at this point, as such an assignment would be redundant. + + int currentSize = _size++; + _version++; + + if (_nodes.Length == currentSize) Grow(currentSize + 1); + + if (_comparer == null) + MoveUpDefaultComparer((element, priority), currentSize); + else + MoveUpCustomComparer((element, priority), currentSize); + } + + /// + /// Returns the minimal element from the without removing it. + /// + /// The is empty. + /// The minimal element of the . + public TElement Peek() + { + if (_size == 0) throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); + + return _nodes[0].Element; + } + + /// + /// Removes and returns the minimal element from the . + /// + /// The queue is empty. + /// The minimal element of the . + public TElement Dequeue() + { + if (_size == 0) throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); + + var element = _nodes[0].Element; + RemoveRootNode(); + return element; + } + + /// + /// Removes the minimal element from the , + /// and copies it to the parameter, + /// and its associated priority to the parameter. + /// + /// The removed element. + /// The priority associated with the removed element. + /// + /// if the element is successfully removed; + /// if the is empty. + /// + public bool TryDequeue([MaybeNullWhen(false)] out TElement element, + [MaybeNullWhen(false)] out TPriority priority) + { + if (_size != 0) + { + (element, priority) = _nodes[0]; + RemoveRootNode(); + return true; + } + + element = default; + priority = default; + return false; + } + + /// + /// Returns a value that indicates whether there is a minimal element in the + /// , + /// and if one is present, copies it to the parameter, + /// and its associated priority to the parameter. + /// The element is not removed from the . + /// + /// The minimal element in the queue. + /// The priority associated with the minimal element. + /// + /// if there is a minimal element; + /// if the is empty. + /// + public bool TryPeek([MaybeNullWhen(false)] out TElement element, + [MaybeNullWhen(false)] out TPriority priority) + { + if (_size != 0) + { + (element, priority) = _nodes[0]; + return true; + } + + element = default; + priority = default; + return false; + } + + /// + /// Adds the specified element with associated priority to the , + /// and immediately removes the minimal element, returning the result. + /// + /// The element to add to the . + /// The priority with which to associate the new element. + /// The minimal element removed after the enqueue operation. + /// + /// Implements an insert-then-extract heap operation that is generally more efficient + /// than sequencing Enqueue and Dequeue operations: in the worst case scenario only one + /// shift-down operation is required. + /// + public TElement EnqueueDequeue(TElement element, TPriority priority) + { + if (_size != 0) + { + var root = _nodes[0]; + + if (_comparer == null) + { + if (Comparer.Default.Compare(priority, root.Priority) > 0) + { + MoveDownDefaultComparer((element, priority), 0); + _version++; + return root.Element; + } + } + else + { + if (_comparer.Compare(priority, root.Priority) > 0) + { + MoveDownCustomComparer((element, priority), 0); + _version++; + return root.Element; + } + } + } + + return element; + } + + /// + /// Enqueues a sequence of element/priority pairs to the . + /// + /// The pairs of elements and priorities to add to the queue. + /// + /// The specified argument was . + /// + public void EnqueueRange(IEnumerable<(TElement Element, TPriority Priority)> items) + { + ArgumentNullException.ThrowIfNull(items); + + int count = 0; + var collection = + items as ICollection<(TElement Element, TPriority Priority)>; + if (collection is not null && (count = collection.Count) > _nodes.Length - _size) Grow(_size + count); + + if (_size == 0) + { + // build using Heapify() if the queue is empty. + + if (collection is not null) + { + collection.CopyTo(_nodes, 0); + _size = count; + } + else + { + int i = 0; + (TElement, TPriority)[] nodes = _nodes; + foreach ((var element, var priority) in items) + { + if (nodes.Length == i) + { + Grow(i + 1); + nodes = _nodes; + } + + nodes[i++] = (element, priority); + } + + _size = i; + } + + _version++; + + if (_size > 1) Heapify(); + } + else + { + foreach ((var element, var priority) in items) Enqueue(element, priority); + } + } + + /// + /// Enqueues a sequence of elements pairs to the , + /// all associated with the specified priority. + /// + /// The elements to add to the queue. + /// The priority to associate with the new elements. + /// + /// The specified argument was . + /// + public void EnqueueRange(IEnumerable elements, TPriority priority) + { + ArgumentNullException.ThrowIfNull(elements); + + int count; + if (elements is ICollection<(TElement Element, TPriority Priority)> collection && + (count = collection.Count) > _nodes.Length - _size) + Grow(_size + count); + + if (_size == 0) + { + // build using Heapify() if the queue is empty. + + int i = 0; + (TElement, TPriority)[] nodes = _nodes; + foreach (var element in elements) + { + if (nodes.Length == i) + { + Grow(i + 1); + nodes = _nodes; + } + + nodes[i++] = (element, priority); + } + + _size = i; + _version++; + + if (i > 1) Heapify(); + } + else + { + foreach (var element in elements) Enqueue(element, priority); + } + } + + /// + /// Removes all items from the . + /// + public void Clear() + { + if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>()) + // Clear the elements so that the gc can reclaim the references + Array.Clear(_nodes, 0, _size); + + _size = 0; + _version++; + } + + /// + /// Ensures that the can hold up to + /// items without further expansion of its backing storage. + /// + /// The minimum capacity to be used. + /// + /// The specified is negative. + /// + /// The current capacity of the . + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum); + + if (_nodes.Length < capacity) + { + Grow(capacity); + _version++; + } + + return _nodes.Length; + } + + /// + /// Sets the capacity to the actual number of items in the , + /// if that is less than 90 percent of current capacity. + /// + /// + /// This method can be used to minimize a collection's memory overhead + /// if no new elements will be added to the collection. + /// + public void TrimExcess() + { + int threshold = (int)(_nodes.Length * 0.9); + if (_size < threshold) + { + Array.Resize(ref _nodes, _size); + _version++; + } + } + + /// + /// Grows the priority queue to match the specified min capacity. + /// + private void Grow(int minCapacity) + { + Debug.Assert(_nodes.Length < minCapacity); + + const int GrowFactor = 2; + const int MinimumGrow = 4; + + int newcapacity = GrowFactor * _nodes.Length; + + // Allow the queue to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _nodes.Length overflowed thanks to the (uint) cast + if ((uint)newcapacity > ArrayEx.MaxLength) newcapacity = ArrayEx.MaxLength; + + // Ensure minimum growth is respected. + newcapacity = Math.Max(newcapacity, _nodes.Length + MinimumGrow); + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newcapacity < minCapacity) newcapacity = minCapacity; + + Array.Resize(ref _nodes, newcapacity); + } + + /// + /// Removes the node from the root of the heap + /// + private void RemoveRootNode() + { + int lastNodeIndex = --_size; + _version++; + + if (lastNodeIndex > 0) + { + var lastNode = _nodes[lastNodeIndex]; + if (_comparer == null) + MoveDownDefaultComparer(lastNode, 0); + else + MoveDownCustomComparer(lastNode, 0); + } + + if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>()) + _nodes[lastNodeIndex] = default; + } + + /// + /// Gets the index of an element's parent. + /// + private static int GetParentIndex(int index) => (index - 1) >> Log2Arity; + + /// + /// Gets the index of the first child of an element. + /// + private static int GetFirstChildIndex(int index) => (index << Log2Arity) + 1; + + /// + /// Converts an unordered list into a heap. + /// + private void Heapify() + { + // Leaves of the tree are in fact 1-element heaps, for which there + // is no need to correct them. The heap property needs to be restored + // only for higher nodes, starting from the first node that has children. + // It is the parent of the very last element in the array. + + var nodes = _nodes; + int lastParentWithChildren = GetParentIndex(_size - 1); + + if (_comparer == null) + for (int index = lastParentWithChildren; index >= 0; --index) + MoveDownDefaultComparer(nodes[index], index); + else + for (int index = lastParentWithChildren; index >= 0; --index) + MoveDownCustomComparer(nodes[index], index); + } + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUpDefaultComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // Instead of swapping items all the way to the root, we will perform + // a similar optimization as in the insertion sort. + + Debug.Assert(_comparer is null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + + while (nodeIndex > 0) + { + int parentIndex = GetParentIndex(nodeIndex); + var parent = nodes[parentIndex]; + + if (Comparer.Default.Compare(node.Priority, parent.Priority) < 0) + { + nodes[nodeIndex] = parent; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + nodes[nodeIndex] = node; + } + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUpCustomComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // Instead of swapping items all the way to the root, we will perform + // a similar optimization as in the insertion sort. + + Debug.Assert(_comparer is not null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var comparer = _comparer; + var nodes = _nodes; + + while (nodeIndex > 0) + { + int parentIndex = GetParentIndex(nodeIndex); + var parent = nodes[parentIndex]; + + if (comparer != null && comparer.Compare(node.Priority, parent.Priority) < 0) + { + nodes[nodeIndex] = parent; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + nodes[nodeIndex] = node; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDownDefaultComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + Debug.Assert(_comparer is null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + int size = _size; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + // Find the child node with the minimal priority + var minChild = nodes[i]; + int minChildIndex = i; + + int childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]; + if (Comparer.Default.Compare(nextChild.Priority, minChild.Priority) < 0) + { + minChild = nextChild; + minChildIndex = i; + } + } + + // Heap property is satisfied; insert node in this location. + if (Comparer.Default.Compare(node.Priority, minChild.Priority) <= 0) break; + + // Move the minimal child up by one node and + // continue recursively from its location. + nodes[nodeIndex] = minChild; + nodeIndex = minChildIndex; + } + + nodes[nodeIndex] = node; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDownCustomComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + Debug.Assert(_comparer is not null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var comparer = _comparer; + var nodes = _nodes; + int size = _size; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + // Find the child node with the minimal priority + var minChild = nodes[i]; + int minChildIndex = i; + + int childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]; + if (comparer != null && comparer.Compare(nextChild.Priority, minChild.Priority) < 0) + { + minChild = nextChild; + minChildIndex = i; + } + } + + // Heap property is satisfied; insert node in this location. + if (comparer != null && comparer.Compare(node.Priority, minChild.Priority) <= 0) break; + + // Move the minimal child up by one node and continue recursively from its location. + nodes[nodeIndex] = minChild; + nodeIndex = minChildIndex; + } + + nodes[nodeIndex] = node; + } + + /// + /// Initializes the custom comparer to be used internally by the heap. + /// + private static IComparer? InitializeComparer(IComparer? comparer) + { + if (typeof(TPriority).IsValueType) + { + if (comparer == Comparer.Default) + // if the user manually specifies the default comparer, + // revert to using the optimized path. + return null; + + return comparer; + } + + // Currently the JIT doesn't optimize direct Comparer.Default.Compare + // calls for reference types, so we want to cache the comparer instance instead. + // TODO https://github.com/dotnet/runtime/issues/10050: Update if this changes in the future. + return comparer ?? Comparer.Default; + } + + /// + /// Enumerates the contents of a , without any ordering guarantees. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(PriorityQueueDebugView<,>))] + public sealed class UnorderedItemsCollection : IReadOnlyCollection<(TElement Element, TPriority Priority)>, + ICollection + { + internal readonly PriorityQueue _queue; + + internal UnorderedItemsCollection(PriorityQueue queue) + { + _queue = queue; + } + + object ICollection.SyncRoot => this; + bool ICollection.IsSynchronized => false; + + void ICollection.CopyTo(Array array, int index) + { + ArgumentNullException.ThrowIfNull(array); + + if (array.Rank != 1) throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + + if (array.GetLowerBound(0) != 0) throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array)); + + if (index < 0 || index > array.Length) + throw new ArgumentOutOfRangeException(nameof(index), index, + SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); + + if (array.Length - index < _queue._size) throw new ArgumentException(SR.Argument_InvalidOffLen); + + try + { + Array.Copy(_queue._nodes, 0, array, index, _queue._size); + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } + } + + public int Count => _queue._size; + + IEnumerator<(TElement Element, TPriority Priority)> IEnumerable<(TElement Element, TPriority Priority)>. + GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the . + /// + /// An for the . + public Enumerator GetEnumerator() => new(_queue); + + /// + /// Enumerates the element and priority pairs of a , + /// without any ordering guarantees. + /// + public struct Enumerator : IEnumerator<(TElement Element, TPriority Priority)> + { + private readonly PriorityQueue _queue; + private readonly int _version; + private int _index; + + internal Enumerator(PriorityQueue queue) + { + _queue = queue; + _index = 0; + _version = queue._version; + Current = default; + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + var localQueue = _queue; + + if (_version == localQueue._version && (uint)_index < (uint)localQueue._size) + { + Current = localQueue._nodes[_index]; + _index++; + return true; + } + + return MoveNextRare(); + } + + private bool MoveNextRare() + { + if (_version != _queue._version) + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + + _index = _queue._size + 1; + Current = default; + return false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public (TElement Element, TPriority Priority) Current { get; private set; } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + if (_version != _queue._version) + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + + _index = 0; + Current = default; + } + } + } + } +} \ No newline at end of file diff --git a/NEG/UI/PriorityQueue.cs.meta b/NEG/UI/PriorityQueue.cs.meta new file mode 100644 index 0000000..28b7de0 --- /dev/null +++ b/NEG/UI/PriorityQueue.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1b6a9a70d997fd468f30caa1e760078 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NEG/UI/UiManager.cs b/NEG/UI/UiManager.cs new file mode 100644 index 0000000..028a212 --- /dev/null +++ b/NEG/UI/UiManager.cs @@ -0,0 +1,169 @@ +using JetBrains.Annotations; +using NEG.UI.Area; +using NEG.UI.Popup; +using NEG.UI.Window; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace NEG.UI +{ + [PublicAPI] + public abstract class UiManager + { + public static UiManager Instance { get; private set; } + + /// + /// Current area shown on screen. + /// + public IArea CurrentArea + { + get => currentArea; + set + { + currentArea?.SetEnabled(false); + + currentArea = value; + + currentArea?.SetEnabled(true); + } + } + + /// + /// Current window that is considered main (focused, lastly opened). Can be null. + /// + public IWindow CurrentMainWindow { get; protected set; } + + private IArea currentArea; + private (PopupData data, int priority) currentShownPopup; + protected IDefaultPopup currentDefaultPopup; + + private PriorityQueue popupsToShow = new(); + + //TODO: localize + private string localizedYes = "Yes", localizedNo = "No", localizedOk = "Ok"; + + protected UiManager(IArea startArea) + { + if (Instance != null) + { + Debug.LogError("Only one instance od UiManager is allowed"); + return; + } + + Instance = this; + + CurrentArea = startArea; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing ok button. + /// + /// popup title + /// popup content + /// text to show on ok button, empty for localized "Ok" + /// additional action on ok pressed + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + /// data for created popup, can be used to invalidate popup (will not show) + public PopupData ShowOkPopup(string title, string content, string okText = null, Action okPressed = null, int priority = 0, bool forceShow = false) + { + var data = new DefaultPopupData(currentDefaultPopup, title, content, + new List<(string, Action)>() { (okText ?? localizedOk, okPressed) }); + ShowPopup(data, priority, forceShow); + return data; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing yes or no button. + /// + /// popup title + /// popup content + /// text to show on yes button, empty for localized "Yes" + /// text to show on no button, empty for localized "No" + /// additional action on yes pressed + /// additional action on no pressed + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + /// data for created popup, can be used to invalidate popup (will not show) + public PopupData ShowYesNoPopup(string title, string content, string yesText = null, string noText = null, Action yesPressed = null, Action noPressed = null, int priority = 0, bool forceShow = false) + { + var data = new DefaultPopupData(currentDefaultPopup, title, content, + new List<(string, Action)>() { (yesText ?? localizedYes, yesPressed), (noText ?? localizedNo, noPressed) }); + ShowPopup(data, priority, forceShow); + return data; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing any button. + /// + /// popup title + /// popup content + /// list of actions + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + /// data for created popup, can be used to invalidate popup (will not show) + public PopupData ShowPopup(string title, string content, List<(string, Action)> actions, int priority = 0, bool forceShow = false) + { + var data = new DefaultPopupData(currentDefaultPopup, title, content, actions); + ShowPopup(data, priority, forceShow); + return data; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. + /// + /// popup data object + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + public void ShowPopup(PopupData data, int priority = 0, bool forceShow = false) + { + popupsToShow.Enqueue(data, priority); + UpdatePopupsState(forceShow, priority, data); + } + + protected void PopupClosed(PopupData data) + { + if (currentShownPopup.data != data) + { + Debug.LogError("Popup was not shown"); + return; + } + UpdatePopupsState(false); + } + + protected void SetDefaultPopup(IDefaultPopup popup) => currentDefaultPopup = popup; + + private void UpdatePopupsState(bool forceShow, int priority = 0, PopupData data = null) + { + if (forceShow) + { + if(currentShownPopup.priority <= priority) + return; + + popupsToShow.Enqueue(currentShownPopup.data, currentShownPopup.priority); + ShowPopup(data, priority); + return; + } + + if(!popupsToShow.TryDequeue(out var d, out int p)) + return; + + ShowPopup(d, p); + } + + private void ShowPopup(PopupData data, int priority) + { + if (currentShownPopup.data != null) + { + currentShownPopup.data.PopupClosedEvent -= PopupClosed; + currentShownPopup.data.Hide(); + } + currentShownPopup = (data, priority); + data.Show(); + data.PopupClosedEvent += PopupClosed; + } + } +} + + diff --git a/NEG/UI/UiManager.cs.meta b/NEG/UI/UiManager.cs.meta new file mode 100644 index 0000000..b87a94a --- /dev/null +++ b/NEG/UI/UiManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c322beedd2ec8f844903c18b1ef74b15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NEG/UI/UnityUi.meta b/NEG/UI/UnityUi.meta new file mode 100644 index 0000000..beaa12a --- /dev/null +++ b/NEG/UI/UnityUi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0be272f427b40848b8aeff5b04770b4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NEG/UI/UnityUi/Area.meta b/NEG/UI/UnityUi/Area.meta new file mode 100644 index 0000000..abc61b9 --- /dev/null +++ b/NEG/UI/UnityUi/Area.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a99aed857b7b9e7459811b14fefdb04f +timeCreated: 1670706939 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Area/AutoWindowOpen.cs b/NEG/UI/UnityUi/Area/AutoWindowOpen.cs new file mode 100644 index 0000000..b19f017 --- /dev/null +++ b/NEG/UI/UnityUi/Area/AutoWindowOpen.cs @@ -0,0 +1,15 @@ +using NEG.UI.UnityUi.Window; +using NEG.UI.Window; +using System; +using UnityEngine; + +namespace NEG.UI.Area +{ + [Tooltip(tooltip: "Automatically open attached window on start")] + public class AutoWindowOpen : MonoBehaviour + { + [SerializeField] private MonoWindow window; + + private void Start() => window.Open(); + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/Area/AutoWindowOpen.cs.meta b/NEG/UI/UnityUi/Area/AutoWindowOpen.cs.meta new file mode 100644 index 0000000..b3ed13c --- /dev/null +++ b/NEG/UI/UnityUi/Area/AutoWindowOpen.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e439a77332dc4e3a881348c9506556c6 +timeCreated: 1673791915 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Area/MonoArea.cs b/NEG/UI/UnityUi/Area/MonoArea.cs new file mode 100644 index 0000000..e5f92b0 --- /dev/null +++ b/NEG/UI/UnityUi/Area/MonoArea.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using UnityEngine; +using NEG.UI.Popup; +using NEG.UI.UnityUi.Window; +using NEG.UI.UnityUi.WindowSlot; +using NEG.UI.Window; +using NEG.UI.WindowSlot; +using System; + +namespace NEG.UI.Area +{ + public class MonoArea : MonoBehaviour, IArea + { + public IEnumerable AvailableSlots => windowSlots; + public IWindowSlot DefaultWindowSlot => windowSlots[0]; + + [SerializeField] private bool setAsDefaultArea; + + [SerializeField] private List windowSlots; + + public virtual void SetEnabled(bool setEnabled) => gameObject.SetActive(setEnabled); + + public virtual void OpenWindow(IWindow window, object data = null) + { + DefaultWindowSlot.AttachWindow(window); + window.SetData(data); + } + + protected virtual void Awake() + { + if (setAsDefaultArea) + UiManager.Instance.CurrentArea = this; + } + + protected virtual void OnDestroy() + { + if (ReferenceEquals(UiManager.Instance.CurrentArea, this)) + UiManager.Instance.CurrentArea = null; + } + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/Area/MonoArea.cs.meta b/NEG/UI/UnityUi/Area/MonoArea.cs.meta new file mode 100644 index 0000000..958b733 --- /dev/null +++ b/NEG/UI/UnityUi/Area/MonoArea.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 39eb59ca1ef60934abb3f0c64169be65 +timeCreated: 1670707479 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons.meta b/NEG/UI/UnityUi/Buttons.meta new file mode 100644 index 0000000..edba027 --- /dev/null +++ b/NEG/UI/UnityUi/Buttons.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dcaa262d2bcd4b19aa25512a19830555 +timeCreated: 1670777190 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/BaseButton.cs b/NEG/UI/UnityUi/Buttons/BaseButton.cs new file mode 100644 index 0000000..35dfbe9 --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/BaseButton.cs @@ -0,0 +1,78 @@ +using FMOD.Studio; +using FMODUnity; +using System; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace NEG.UI.UnityUi.Buttons +{ + [RequireComponent(typeof(ButtonSerializeFields))] + public class BaseButton : MonoBehaviour, ISelectHandler, IDeselectHandler, IPointerEnterHandler, IPointerExitHandler + { + public event Action OnButtonPressed; + + public bool Interactable { get => serializeFields.Button.interactable; set => serializeFields.Button.interactable = value; } + + [SerializeField] + protected ButtonSerializeFields serializeFields; + + private bool isHovered; + + public virtual void OnSelect(BaseEventData eventData) + { + if (serializeFields.Text) + serializeFields.Text.color = serializeFields.SelectedTextColor; + } + + public void OnDeselect(BaseEventData eventData) + { + if (serializeFields.Text) + serializeFields.Text.color = serializeFields.DeselectedTextColor; + } + + public void OnPointerEnter(PointerEventData eventData) + { + isHovered = true; + if (serializeFields.Text) + serializeFields.Text.color = serializeFields.SelectedTextColor; + } + + public void OnPointerExit(PointerEventData eventData) + { + isHovered = false; + if (serializeFields.Text) + serializeFields.Text.color = serializeFields.DeselectedTextColor; + } + + public void SetText(string text) + { + if(serializeFields == null) + return; + if(serializeFields.Text == null) + return; + serializeFields.Text.text = text; + } + + protected virtual void Awake() + { + if(serializeFields == null) + serializeFields = GetComponent(); + serializeFields.Button.onClick.AddListener(OnClicked); + OnDeselect(null); + } + + private void OnValidate() + { + if(serializeFields == null) + serializeFields = GetComponent(); + } + + protected virtual void OnClicked() + { + OnDeselect(null); + isHovered = false; + OnButtonPressed?.Invoke(); + } + } +} diff --git a/NEG/UI/UnityUi/Buttons/BaseButton.cs.meta b/NEG/UI/UnityUi/Buttons/BaseButton.cs.meta new file mode 100644 index 0000000..ea42d55 --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/BaseButton.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3a250ef2d0c34e7396a16fc5eddbdb01 +timeCreated: 1670777213 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/ButtonReaction.cs b/NEG/UI/UnityUi/Buttons/ButtonReaction.cs new file mode 100644 index 0000000..9e8bf17 --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/ButtonReaction.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + +namespace NEG.UI.UnityUi.Buttons +{ + [RequireComponent(typeof(BaseButton))] + public abstract class ButtonReaction : MonoBehaviour + { + private void Awake() => GetComponent().OnButtonPressed += OnClicked; + + private void OnDestroy() => GetComponent().OnButtonPressed -= OnClicked; + + protected abstract void OnClicked(); + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/ButtonReaction.cs.meta b/NEG/UI/UnityUi/Buttons/ButtonReaction.cs.meta new file mode 100644 index 0000000..1089ca5 --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/ButtonReaction.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a9761128a04b49c2a26eddfabe70331f +timeCreated: 1675707257 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/ButtonSerializeFields.cs b/NEG/UI/UnityUi/Buttons/ButtonSerializeFields.cs new file mode 100644 index 0000000..60e93a9 --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/ButtonSerializeFields.cs @@ -0,0 +1,33 @@ +using FMODUnity; +using System; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace NEG.UI.UnityUi.Buttons +{ + public class ButtonSerializeFields : MonoBehaviour + { + [field: SerializeField] + public Button Button { get; private set; } + [field: SerializeField] + public TMP_Text Text { get; private set; } + [field: SerializeField] + public Color SelectedTextColor { get; private set; } + [field: SerializeField] + public Color DeselectedTextColor { get; private set; } + [field: SerializeField] + public EventReference HoverEventRef { get; private set; } + [field: SerializeField] + public EventReference ClickEventRef { get; private set; } + + private void OnValidate() + { + if(Button == null) + Button = GetComponent