diff --git a/.idea/.idea.ca2d-unity-toolkit/.idea/vcs.xml b/.idea/.idea.ca2d-unity-toolkit/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.ca2d-unity-toolkit/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Editor.meta b/Assets/Toolkit/Toolkit/Editor.meta new file mode 100644 index 0000000..a55e6e2 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9fbae8b6b0a64df885641b3584fcb15b +timeCreated: 1719991315 \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Editor/Scripts.meta b/Assets/Toolkit/Toolkit/Editor/Scripts.meta new file mode 100644 index 0000000..cecb9f4 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Editor/Scripts.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce8a0f7441b34ce9a8f891b87fbaad91 +timeCreated: 1719991325 \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Editor/Scripts/AssetDatabaseUtility.cs b/Assets/Toolkit/Toolkit/Editor/Scripts/AssetDatabaseUtility.cs new file mode 100644 index 0000000..3aa6de6 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Editor/Scripts/AssetDatabaseUtility.cs @@ -0,0 +1,105 @@ +using System.Text; +using UnityEditor; +using UnityEngine; + +namespace Ca2d.Toolkit.Editor +{ + public static class AssetDatabaseUtility + { + /// + /// Check if this path contains a valid asset. + /// + /// Target path + /// True that path is an asset + public static bool PathIsValidAsset(string path) + { + return AssetDatabase.IsValidFolder(path) == false && + string.IsNullOrWhiteSpace(AssetDatabase.AssetPathToGUID(path)) == false; + } + + /// + /// Create a folder in a path if this folder has not been created. + /// + /// Target folder path + /// Is this path is ready to be a folder? + public static bool CreateFolderIfNotExist(string folderPath) + { + if (PathIsValidAsset(folderPath)) return false; + if (AssetDatabase.IsValidFolder(folderPath)) return true; + + var pathVector = folderPath.Split('/'); + if (pathVector.Length == 0) return false; + if (pathVector.Length == 1) return AssetDatabase.IsValidFolder(pathVector[0]); + if (pathVector.Length < 2) + { + if (AssetDatabase.IsValidFolder($"{pathVector[0]}/{pathVector[1]}")) return true; + return string.IsNullOrEmpty(AssetDatabase.CreateFolder(pathVector[0], pathVector[1])) == false; + } + + var sb = new StringBuilder(pathVector[0]); + var prev = pathVector[0]; + for (var i = 1; i < pathVector.Length; i++) + { + var tk = pathVector[i]; + if (i == pathVector.Length - 1 && tk.Contains('.')) continue; + sb.Append('/').Append(tk); + + var cur = sb.ToString(); + if (AssetDatabase.IsValidFolder(cur) == false) + { + if (string.IsNullOrEmpty(AssetDatabase.CreateFolder(prev, tk))) + return false; + } + + prev = cur; + } + + return true; + } + + /// + /// Create asset at path with everything done (include create path and create asset.). + /// + /// The folder path this file will be create to. + /// The type of asset. + /// If asset path is valid, return new asset. + public static T CreateAssetAtPath(string path) where T : ScriptableObject + { + if (CreateFolderIfNotExist(path) == false) return null; + var asset = ScriptableObject.CreateInstance(); + + AssetDatabase.CreateAsset(asset, $"{path}"); + + return asset; + } + + /// + /// Create asset at path with everything done (include create path and create asset.). + /// + /// The folder path this file will be create to. + /// The file name this asset will be. + /// The type of asset. + /// If asset path is valid, return new asset. + public static T CreateAssetAtPath(string path, string name) where T : ScriptableObject + { + if (CreateFolderIfNotExist(path) == false) return null; + var asset = ScriptableObject.CreateInstance(); + + if (path.EndsWith('/')) AssetDatabase.CreateAsset(asset, $"{path}{name}.asset"); + else AssetDatabase.CreateAsset(asset, $"{path}/{name}.asset"); + + return asset; + } + + /// + /// Trim an editor path to resource path. + /// + /// Editor path + public static void AssetPathTrimmer(ref string path) + { + path = path.Replace("Assets/Resources/", ""); + path = path.Replace(".asset", ""); + path = path.Replace(".prefab", ""); + } + } +} \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Editor/Scripts/AssetDatabaseUtility.cs.meta b/Assets/Toolkit/Toolkit/Editor/Scripts/AssetDatabaseUtility.cs.meta new file mode 100644 index 0000000..1e7e6da --- /dev/null +++ b/Assets/Toolkit/Toolkit/Editor/Scripts/AssetDatabaseUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97def5fd9acbf7db4a7e5cee4537f1c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Toolkit/Toolkit/Editor/Scripts/OptionDrawer.cs b/Assets/Toolkit/Toolkit/Editor/Scripts/OptionDrawer.cs new file mode 100644 index 0000000..942e8e8 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Editor/Scripts/OptionDrawer.cs @@ -0,0 +1,35 @@ +using UnityEditor; +using UnityEngine; + +namespace Ca2d.Toolkit.Editor +{ + [CustomPropertyDrawer(typeof(Option<>))] + public class OptionDrawer : PropertyDrawer + { + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return EditorGUI.GetPropertyHeight(property.FindPropertyRelative("Value")); ; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + var value = property.FindPropertyRelative("Value"); + var enable = property.FindPropertyRelative("Enabled"); + + var toggleRect = position; + toggleRect.height = toggleRect.width = 18; + + enable.boolValue = EditorGUI.Toggle(toggleRect, GUIContent.none, enable.boolValue); + var prev = GUI.enabled; + GUI.enabled = enable.boolValue; + + var propertyRect = position; + propertyRect.x += 18; + propertyRect.width -= 18; + EditorGUI.PropertyField(propertyRect, value, label, true); + GUI.enabled = prev; + + if (value.serializedObject.hasModifiedProperties) value.serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Editor/Scripts/OptionDrawer.cs.meta b/Assets/Toolkit/Toolkit/Editor/Scripts/OptionDrawer.cs.meta new file mode 100644 index 0000000..110390b --- /dev/null +++ b/Assets/Toolkit/Toolkit/Editor/Scripts/OptionDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f242342c346b40fb2836611534f4e953 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Toolkit/Toolkit/Scripts/Guard.cs b/Assets/Toolkit/Toolkit/Scripts/Guard.cs index fa98e80..3e1c8c3 100644 --- a/Assets/Toolkit/Toolkit/Scripts/Guard.cs +++ b/Assets/Toolkit/Toolkit/Scripts/Guard.cs @@ -11,7 +11,9 @@ namespace Ca2d.Toolkit private static void IgnoreNoTarget() { +#if UNITY_EDITOR Debug.LogError("Ignore Exception should run with executor!"); +#endif } #region IgnoreException(Action) diff --git a/Assets/Toolkit/Toolkit/Scripts/ILoggHandler.cs b/Assets/Toolkit/Toolkit/Scripts/ILoggHandler.cs index 27043ea..51ae23d 100644 --- a/Assets/Toolkit/Toolkit/Scripts/ILoggHandler.cs +++ b/Assets/Toolkit/Toolkit/Scripts/ILoggHandler.cs @@ -1,3 +1,4 @@ +#if ENABLE_LOGG using UnityEngine; namespace Ca2d.Toolkit @@ -40,4 +41,5 @@ namespace Ca2d.Toolkit public void ClearLabel(); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/Logg.cs b/Assets/Toolkit/Toolkit/Scripts/Logg.cs index ea40fb4..9d53772 100644 --- a/Assets/Toolkit/Toolkit/Scripts/Logg.cs +++ b/Assets/Toolkit/Toolkit/Scripts/Logg.cs @@ -1,3 +1,4 @@ +#if ENABLE_LOGG using System; using System.Diagnostics; using System.Text; @@ -279,4 +280,5 @@ namespace Ca2d.Toolkit default(Logg).Error(err, context); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/LoggHandler.cs b/Assets/Toolkit/Toolkit/Scripts/LoggHandler.cs index add3dbd..f9aa1b9 100644 --- a/Assets/Toolkit/Toolkit/Scripts/LoggHandler.cs +++ b/Assets/Toolkit/Toolkit/Scripts/LoggHandler.cs @@ -1,3 +1,4 @@ +#if ENABLE_LOGG using System; using System.Diagnostics; using System.Text; @@ -99,4 +100,5 @@ namespace Ca2d.Toolkit m_innerHandle = innerHandle ?? throw new ArgumentNullException(nameof(innerHandle)); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/ResLock.cs b/Assets/Toolkit/Toolkit/Scripts/ResLock.cs new file mode 100644 index 0000000..4473cf9 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Scripts/ResLock.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Ca2d.Toolkit +{ + + public delegate void ResLockStateChanged(ResLock resLock, uint notionId, bool notionSelfState); + + public class ResLock + { + private struct ResourceNotionCore : IDisposable + { + + #region Allocator + + private static SortedSet m_allocateMap = new(); + + private static uint AllocateId() + { + // Id should start at 1 to avoid 0 as default. + uint id = 1; + foreach (var u in m_allocateMap) + { + if (id < u) break; + id = u + 1; + } + + m_allocateMap.Add(id); + return id; + } + + private static void FreeId(uint id) + { + m_allocateMap.Remove(id); + } + + #endregion + + #region Notification + + + private static bool _LockStateNotifyGuard = false; + + private static void NotifyLoopDetect() + { + if (_LockStateNotifyGuard) throw new InvalidOperationException(); + } + + private static void Notify(ResLock target, ref ResourceNotionCore core) + { + _LockStateNotifyGuard = true; + + try + { + _LockStateEvents?.Invoke(target, core.Id, core.Target.m_resNotions.Contains(core.Id)); + } + catch (Exception e) + { + Debug.LogError(e); + } + + _LockStateNotifyGuard = false; + } + + + #endregion + + public readonly uint Id; + + public readonly ResLock Target; + + public ResourceNotionCore(ResLock target) + { + Target = target; + Id = AllocateId(); +#if DEBUG + Debug.LogFormat("Resource Notion '{0}' was allocated for {1}", Id, target.FriendlyName); +#endif + } + + public bool SetState(bool enabled) + { + NotifyLoopDetect(); + + var result = enabled ? Target.m_resNotions.Add(Id) : Target.m_resNotions.Remove(Id); + Notify(Target, ref this); + + return result; + } + + public void Dispose() + { + FreeId(Id); +#if DEBUG + Debug.LogFormat("Resource Notion '{0}' was released from {1}", Id, Target.FriendlyName); +#endif + } + } + + public struct LiteResourceNotion : IDisposable + { + private ResourceNotionCore m_core; + + internal LiteResourceNotion(ResLock parent) + { + m_core = new ResourceNotionCore(parent); + m_core.SetState(true); + } + + public void Dispose() + { + if (m_core.Target == null) + throw new InvalidOperationException("Can not dispose a disposed Resource Notion!"); + + m_core.SetState(false); + m_core.Dispose(); + m_core = default; + } + } + + + public class ResourceNotion : IDisposable + { + private ResourceNotionCore m_core; + + /// + /// Event when locking state changed. + /// + public event Action OnStateChanged; + + /// + /// The locking state of this notion. + /// + public bool LockStateSelf { get; private set; } + + /// + /// The parent of this notion. + /// + public ResLock Parent => m_core.Target; + + internal ResourceNotion(ResLock parent) + { + m_core = new ResourceNotionCore(parent); + LockStateSelf = false; + _LockStateEvents += NotifyListener; + } + + /// + /// Lock this notion + /// + public bool Lock() + { + Guard(); + if (m_core.SetState(true)) + { + LockStateSelf = true; + return true; + } + + return false; + } + + /// + /// Unlock this notion. + /// + public bool Unlock() + { + Guard(); + if (m_core.SetState(false)) + { + LockStateSelf = false; + return true; + } + + return false; + } + + public void Dispose() + { + if (m_core.Target == null) + throw new InvalidOperationException("Can not dispose a disposed Resource Notion!"); + + m_core.SetState(false); + _LockStateEvents -= NotifyListener; + LockStateSelf = false; + m_core.Dispose(); + m_core = default; + } + + private void NotifyListener(ResLock target, uint id, bool selfState) + { + if (target == Parent) + { + OnStateChanged?.Invoke(target.LockState); + } + } + + private void Guard() + { + if (m_core.Target == null) + throw new InvalidOperationException("Can not operate a disposed Resource Notion!"); + } + } + + private static event ResLockStateChanged _LockStateEvents +#if DEBUG + = (locker, id, selfState) => + { + var name = locker.FriendlyName; + var state = locker.LockState; + + using (StringBuilderPool.Get(out var sb)) + { + sb.AppendFormat( + "Resource Notion '{0}' changes to {1} made Locker '{2}' state changed to {3} ", + id, + selfState ? "True" : "False", + name, + state ? "True" : "False"); + + sb.AppendFormat("(limit {0}:, used: {1})", locker.m_resCount, locker.m_resNotions.Count); + + Debug.Log(sb); + } + } +#endif + ; + + private uint m_resCount; + + private HashSet m_resNotions = new (); + + public ResLock(uint resCount = 0, string friendlyName = default) + { + m_resCount = resCount; + FriendlyName = friendlyName; + } + + /// + /// User friendly name for ResLock + /// + public string FriendlyName { get; } + + /// + /// State of this lock itself + /// + public bool LockState => m_resNotions.Count > m_resCount; + + /// + /// Do locking on this locker + /// + /// Should require this lock is free + /// Locker Notion can be used in using block + /// Lock can not being operated. + public LiteResourceNotion Lock(bool requireLockStateIsFree = true) + { + if (requireLockStateIsFree && LockState) + { + if (string.IsNullOrWhiteSpace(FriendlyName)) throw new InvalidOperationException( + "Can not lock Resource Lock due to free state is required."); + else throw new InvalidOperationException( + $"Can not lock Resource Lock '{FriendlyName}' due to free state is required."); + } + + return new LiteResourceNotion(this); + } + + /// + /// 获取一个锁注解 + /// + /// 该注解的起始锁定状态 + public ResourceNotion GetNotion(bool initialLock = false) + { + var n = new ResourceNotion(this); + if (initialLock) n.Lock(); + + return n; + } + } +} \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/ResLock.cs.meta b/Assets/Toolkit/Toolkit/Scripts/ResLock.cs.meta new file mode 100644 index 0000000..56458fb --- /dev/null +++ b/Assets/Toolkit/Toolkit/Scripts/ResLock.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 28e7e62b954e414f80499b70d8e3dcf3 +timeCreated: 1719992451 \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/StringBuilderPool.cs b/Assets/Toolkit/Toolkit/Scripts/StringBuilderPool.cs new file mode 100644 index 0000000..bfc4579 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Scripts/StringBuilderPool.cs @@ -0,0 +1,18 @@ +using System.Text; +using UnityEngine.Pool; + +namespace Ca2d.Toolkit +{ + public static class StringBuilderPool + { + private static readonly ObjectPool s_Pool = new ( + () => new StringBuilder(), + sb => sb.Clear()); + + public static StringBuilder Get() => s_Pool.Get(); + + public static PooledObject Get(out StringBuilder value) => s_Pool.Get(out value); + + public static void Release(StringBuilder toRelease) => s_Pool.Release(toRelease); + } +} \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/StringBuilderPool.cs.meta b/Assets/Toolkit/Toolkit/Scripts/StringBuilderPool.cs.meta new file mode 100644 index 0000000..b290bc5 --- /dev/null +++ b/Assets/Toolkit/Toolkit/Scripts/StringBuilderPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41341c104f749b437ad0e367dd9a0d0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Toolkit/Toolkit/Scripts/UnityObjectWarp.cs b/Assets/Toolkit/Toolkit/Scripts/UnityObjectWarp.cs deleted file mode 100644 index d19c76c..0000000 --- a/Assets/Toolkit/Toolkit/Scripts/UnityObjectWarp.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using UnityEngine; - -namespace Ca2d.Toolkit -{ - /// - /// An utility which allows you to reference an via it's interface. - /// - /// The type of class which you are willing to reference as. - [Serializable] - public struct UnityObjectWarp where T : class - { - [SerializeField] private UnityEngine.Object m_referencedObject; - - /// - /// Is this warp reference to a valid target? - /// - public bool Is => m_referencedObject is T; - - /// - /// Trying to get the actual object of this reference. - /// - public T As => m_referencedObject as T; - - /// - /// Is this warp reference to a valid target? If this is valid, give the casting result. - /// - public bool IsAs(out T val) - { - if (m_referencedObject is T referenced) - { - val = referenced; - return true; - } - - val = null; - return false; - } - } -} \ No newline at end of file diff --git a/Assets/Toolkit/Toolkit/Scripts/UnityObjectWarp.cs.meta b/Assets/Toolkit/Toolkit/Scripts/UnityObjectWarp.cs.meta deleted file mode 100644 index ca3a245..0000000 --- a/Assets/Toolkit/Toolkit/Scripts/UnityObjectWarp.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 94c950f0a2014abc9e3f97281d149318 -timeCreated: 1713286510 \ No newline at end of file diff --git a/Assets/Toolkit/package.json b/Assets/Toolkit/package.json index 8f28573..8a8f22d 100644 --- a/Assets/Toolkit/package.json +++ b/Assets/Toolkit/package.json @@ -1,6 +1,6 @@ { "name": "xyz.ca2didi.unity.toolkit", - "version": "0.1.0", + "version": "0.1.1", "displayName": "Ca2D Toolkit", "description": "Set of toolkits which can boost your develop experience and speed in Unity.", "unity": "2021.3", diff --git a/Ca2d.Toolkit.csproj.DotSettings b/Ca2d.Toolkit.csproj.DotSettings index 05c8e83..6e6c12c 100644 --- a/Ca2d.Toolkit.csproj.DotSettings +++ b/Ca2d.Toolkit.csproj.DotSettings @@ -4,6 +4,7 @@ True True True + True True True True \ No newline at end of file