com.wallstop-studios.unity-helpers 2.0.0-rc76.1 → 2.0.0-rc76.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,359 @@
1
+ namespace WallstopStudios.UnityHelpers.Core.DataStructure
2
+ {
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.Linq;
6
+ using System.Text;
7
+ using UnityEngine;
8
+
9
+ /// <summary>
10
+ /// A highly optimized, array-backed Trie implementation for fast prefix search and exact word lookup.
11
+ /// Preallocates storage based on total characters in the input set and uses integer indices for traversal,
12
+ /// minimizing memory allocations and indirections. Provides allocation-free prefix search method (aside from
13
+ /// returned string allocations).
14
+ /// </summary>
15
+ public sealed class Trie
16
+ {
17
+ private const int Poison = -1;
18
+
19
+ private readonly char[] _chars;
20
+ private readonly int[] _firstChild;
21
+ private readonly int[] _nextSibling;
22
+ private readonly bool[] _isWord;
23
+ private readonly StringBuilder _stringBuilder;
24
+ private int _nodeCount;
25
+
26
+ /// <summary>
27
+ /// Constructs the Trie from the provided collection of words.
28
+ /// </summary>
29
+ /// <param name="words">All possible words to insert into the Trie.</param>
30
+ public Trie(IEnumerable<string> words)
31
+ {
32
+ IReadOnlyList<string> wordList = words as IReadOnlyList<string> ?? words.ToList();
33
+ int maxWordLength;
34
+ if (wordList.Count > 0)
35
+ {
36
+ maxWordLength = wordList[0].Length;
37
+ for (int i = 1; i < wordList.Count; ++i)
38
+ {
39
+ maxWordLength = Mathf.Max(maxWordLength, wordList[i].Length);
40
+ }
41
+ }
42
+ else
43
+ {
44
+ maxWordLength = 0;
45
+ }
46
+
47
+ int capacity = 1;
48
+ for (int i = 0; i < wordList.Count; ++i)
49
+ {
50
+ capacity += wordList[i].Length;
51
+ }
52
+
53
+ _chars = new char[capacity];
54
+ _firstChild = new int[capacity];
55
+ _nextSibling = new int[capacity];
56
+ _isWord = new bool[capacity];
57
+
58
+ Array.Fill(_firstChild, Poison);
59
+ Array.Fill(_nextSibling, Poison);
60
+
61
+ _stringBuilder = new StringBuilder(maxWordLength);
62
+
63
+ _nodeCount = 1; // root node index
64
+ for (int i = 0; i < wordList.Count; ++i)
65
+ {
66
+ string word = wordList[i];
67
+ Insert(word);
68
+ }
69
+ }
70
+
71
+ // Inserts a single word into the Trie
72
+ private void Insert(string word)
73
+ {
74
+ int node = 0;
75
+ foreach (char c in word)
76
+ {
77
+ int prev = Poison;
78
+ int child = _firstChild[node];
79
+ while (child != Poison && _chars[child] != c)
80
+ {
81
+ prev = child;
82
+ child = _nextSibling[child];
83
+ }
84
+ if (child == Poison)
85
+ {
86
+ child = _nodeCount++;
87
+ _chars[child] = c;
88
+ _firstChild[child] = Poison;
89
+ _nextSibling[child] = Poison;
90
+ if (prev == Poison)
91
+ {
92
+ _firstChild[node] = child;
93
+ }
94
+ else
95
+ {
96
+ _nextSibling[prev] = child;
97
+ }
98
+ }
99
+ node = child;
100
+ }
101
+ _isWord[node] = true;
102
+ }
103
+
104
+ /// <summary>
105
+ /// Determines whether the exact word exists in the Trie.
106
+ /// </summary>
107
+ public bool Contains(string word)
108
+ {
109
+ int node = 0;
110
+ foreach (char c in word)
111
+ {
112
+ int child = _firstChild[node];
113
+ while (child != Poison && _chars[child] != c)
114
+ {
115
+ child = _nextSibling[child];
116
+ }
117
+
118
+ if (child == Poison)
119
+ {
120
+ return false;
121
+ }
122
+
123
+ node = child;
124
+ }
125
+ return _isWord[node];
126
+ }
127
+
128
+ /// <summary>
129
+ /// Collects up to maxResults words that start with the given prefix.
130
+ /// Results are added into the provided list (which is cleared at the start).
131
+ /// Returns the number of results added.
132
+ /// </summary>
133
+ public int GetWordsWithPrefix(
134
+ string prefix,
135
+ List<string> results,
136
+ int maxResults = int.MaxValue
137
+ )
138
+ {
139
+ results.Clear();
140
+ int node = 0;
141
+ foreach (char c in prefix)
142
+ {
143
+ int child = _firstChild[node];
144
+ while (child != Poison && _chars[child] != c)
145
+ {
146
+ child = _nextSibling[child];
147
+ }
148
+
149
+ if (child == Poison)
150
+ {
151
+ return 0;
152
+ }
153
+
154
+ node = child;
155
+ }
156
+ _stringBuilder.Clear();
157
+ _stringBuilder.Append(prefix);
158
+ Collect(node, results, maxResults);
159
+ return results.Count;
160
+ }
161
+
162
+ // Recursive collection without allocations
163
+ private void Collect(int node, List<string> results, int maxResults)
164
+ {
165
+ if (results.Count >= maxResults)
166
+ {
167
+ return;
168
+ }
169
+
170
+ if (_isWord[node])
171
+ {
172
+ results.Add(_stringBuilder.ToString());
173
+ if (results.Count >= maxResults)
174
+ {
175
+ return;
176
+ }
177
+ }
178
+ for (int child = _firstChild[node]; child != Poison; child = _nextSibling[child])
179
+ {
180
+ _stringBuilder.Append(_chars[child]);
181
+ Collect(child, results, maxResults);
182
+ _stringBuilder.Length--;
183
+ if (results.Count >= maxResults)
184
+ {
185
+ return;
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ /// <summary>
192
+ /// A highly optimized, array-backed generic Trie for mapping string keys to values of type T.
193
+ /// Preallocates storage based on total characters in the key set and uses integer indices for traversal,
194
+ /// minimizing memory allocations and indirections. Provides allocation-free prefix search (aside from
195
+ /// the output list allocations themselves).
196
+ /// </summary>
197
+ public sealed class Trie<T>
198
+ {
199
+ private const int Poison = -1;
200
+
201
+ private readonly char[] _chars;
202
+ private readonly int[] _firstChild;
203
+ private readonly int[] _nextSibling;
204
+ private readonly bool[] _hasValue;
205
+ private readonly T[] _values;
206
+ private int _nodeCount;
207
+
208
+ /// <summary>
209
+ /// Constructs the Trie from the provided dictionary of keys to values.
210
+ /// </summary>
211
+ /// <param name="items">Mapping from unique string keys to values of type T.</param>
212
+ public Trie(IReadOnlyDictionary<string, T> items)
213
+ {
214
+ KeyValuePair<string, T>[] array = items.ToArray();
215
+ int capacity = 1;
216
+ foreach (KeyValuePair<string, T> entry in array)
217
+ {
218
+ capacity += entry.Key.Length;
219
+ }
220
+
221
+ _chars = new char[capacity];
222
+ _firstChild = new int[capacity];
223
+ _nextSibling = new int[capacity];
224
+ _hasValue = new bool[capacity];
225
+ _values = new T[capacity];
226
+
227
+ Array.Fill(_firstChild, Poison);
228
+ Array.Fill(_nextSibling, Poison);
229
+
230
+ _nodeCount = 1;
231
+ foreach (KeyValuePair<string, T> kv in array)
232
+ {
233
+ Insert(kv.Key, kv.Value);
234
+ }
235
+ }
236
+
237
+ // Inserts a single key-value pair into the Trie
238
+ private void Insert(string key, T value)
239
+ {
240
+ int node = 0;
241
+ foreach (char c in key)
242
+ {
243
+ int prev = Poison;
244
+ int child = _firstChild[node];
245
+ while (child != Poison && _chars[child] != c)
246
+ {
247
+ prev = child;
248
+ child = _nextSibling[child];
249
+ }
250
+ if (child == Poison)
251
+ {
252
+ child = _nodeCount++;
253
+ _chars[child] = c;
254
+ _firstChild[child] = Poison;
255
+ _nextSibling[child] = Poison;
256
+ if (prev == Poison)
257
+ {
258
+ _firstChild[node] = child;
259
+ }
260
+ else
261
+ {
262
+ _nextSibling[prev] = child;
263
+ }
264
+ }
265
+ node = child;
266
+ }
267
+ _hasValue[node] = true;
268
+ _values[node] = value;
269
+ }
270
+
271
+ /// <summary>
272
+ /// Attempts to retrieve the value associated with the exact key.
273
+ /// </summary>
274
+ public bool TryGetValue(string key, out T value)
275
+ {
276
+ int node = 0;
277
+ foreach (char c in key)
278
+ {
279
+ int child = _firstChild[node];
280
+ while (child != Poison && _chars[child] != c)
281
+ {
282
+ child = _nextSibling[child];
283
+ }
284
+
285
+ if (child == Poison)
286
+ {
287
+ value = default;
288
+ return false;
289
+ }
290
+ node = child;
291
+ }
292
+ if (_hasValue[node])
293
+ {
294
+ value = _values[node];
295
+ return true;
296
+ }
297
+ value = default;
298
+ return false;
299
+ }
300
+
301
+ /// <summary>
302
+ /// Collects up to maxResults values whose keys start with the given prefix.
303
+ /// Results are added into the provided list (which is cleared at the start).
304
+ /// Returns the number of results added.
305
+ /// </summary>
306
+ public int GetValuesWithPrefix(
307
+ string prefix,
308
+ List<T> results,
309
+ int maxResults = int.MaxValue
310
+ )
311
+ {
312
+ results.Clear();
313
+ int node = 0;
314
+ foreach (char c in prefix)
315
+ {
316
+ int child = _firstChild[node];
317
+ while (child != Poison && _chars[child] != c)
318
+ {
319
+ child = _nextSibling[child];
320
+ }
321
+
322
+ if (child == Poison)
323
+ {
324
+ return 0;
325
+ }
326
+
327
+ node = child;
328
+ }
329
+ Collect(node, results, maxResults);
330
+ return results.Count;
331
+ }
332
+
333
+ // Recursive collection without extra allocations
334
+ private void Collect(int node, List<T> results, int maxResults)
335
+ {
336
+ if (results.Count >= maxResults)
337
+ {
338
+ return;
339
+ }
340
+
341
+ if (_hasValue[node])
342
+ {
343
+ results.Add(_values[node]);
344
+ if (results.Count >= maxResults)
345
+ {
346
+ return;
347
+ }
348
+ }
349
+ for (int child = _firstChild[node]; child != Poison; child = _nextSibling[child])
350
+ {
351
+ Collect(child, results, maxResults);
352
+ if (results.Count >= maxResults)
353
+ {
354
+ return;
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 6693302ec09144868fbf8da3c5f1c298
3
+ timeCreated: 1748539006
@@ -16,8 +16,8 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
16
16
 
17
17
  private static readonly bool UseDensePacking;
18
18
  private static readonly int Min;
19
- private static readonly string[]? DenseNames;
20
- private static readonly Dictionary<int, string>? SparseNames;
19
+ private static readonly string[] DenseNames;
20
+ private static readonly Dictionary<int, string> SparseNames;
21
21
 
22
22
  static EnumNameCache()
23
23
  {
@@ -85,8 +85,8 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
85
85
 
86
86
  private static readonly bool UseDensePacking;
87
87
  private static readonly int Min;
88
- private static readonly string[]? DenseNames;
89
- private static readonly Dictionary<int, string>? SparseNames;
88
+ private static readonly string[] DenseNames;
89
+ private static readonly Dictionary<int, string> SparseNames;
90
90
 
91
91
  static EnumDisplayNameCache()
92
92
  {
@@ -11,6 +11,8 @@
11
11
  {
12
12
  private readonly ConcurrentQueue<Action> _actions = new();
13
13
 
14
+ protected override bool Preserve => false;
15
+
14
16
  #if UNITY_EDITOR
15
17
  private readonly EditorApplication.CallbackFunction _update;
16
18
  private bool _attachedEditorUpdate;
@@ -31,7 +33,7 @@
31
33
  private void OnEnable()
32
34
  {
33
35
  #if UNITY_EDITOR
34
- if (!_attachedEditorUpdate)
36
+ if (!_attachedEditorUpdate && !Application.isPlaying)
35
37
  {
36
38
  EditorApplication.update += _update;
37
39
  _attachedEditorUpdate = true;
@@ -16,14 +16,19 @@
16
16
  private AutoResetEvent _waitHandle;
17
17
  private bool _disposed;
18
18
  private readonly ConcurrentQueue<Exception> _exceptions;
19
+ private readonly TimeSpan _noWorkWaitTime;
19
20
 
20
- public SingleThreadedThreadPool(bool runInBackground = false)
21
+ public SingleThreadedThreadPool(
22
+ bool runInBackground = false,
23
+ TimeSpan? noWorkWaitTime = null
24
+ )
21
25
  {
22
26
  _active = 1;
23
27
  _working = 1;
24
28
  _work = new ConcurrentQueue<Action>();
25
29
  _exceptions = new ConcurrentQueue<Exception>();
26
30
  _waitHandle = new AutoResetEvent(false);
31
+ _noWorkWaitTime = noWorkWaitTime ?? TimeSpan.FromSeconds(1);
27
32
  _worker = new Thread(DoWork) { IsBackground = runInBackground };
28
33
  _worker.Start();
29
34
  }
@@ -95,7 +100,7 @@
95
100
  {
96
101
  try
97
102
  {
98
- _ = _waitHandle?.WaitOne(TimeSpan.FromSeconds(1));
103
+ _ = _waitHandle?.WaitOne(_noWorkWaitTime);
99
104
  }
100
105
  catch (ObjectDisposedException)
101
106
  {
@@ -4,7 +4,6 @@
4
4
  using System.Collections.Generic;
5
5
  using System.Diagnostics;
6
6
  using System.Linq;
7
- using System.Threading;
8
7
  using System.Threading.Tasks;
9
8
  using Core.Extension;
10
9
  using Core.Helper;
@@ -34,7 +34,8 @@
34
34
 
35
35
  TTree quadTree = CreateTree(points);
36
36
 
37
- List<Vector2> pointsInRange = quadTree.GetElementsInRange(center, radius).ToList();
37
+ List<Vector2> pointsInRange = new();
38
+ quadTree.GetElementsInRange(center, radius, pointsInRange);
38
39
  Assert.IsTrue(
39
40
  points.SetEquals(pointsInRange),
40
41
  "Found {0} points in range, expected {1}.",
@@ -46,7 +47,7 @@
46
47
  offset.x -= radius * 2;
47
48
  offset.y -= radius * 2;
48
49
 
49
- pointsInRange = quadTree.GetElementsInRange(offset, radius).ToList();
50
+ quadTree.GetElementsInRange(offset, radius, pointsInRange);
50
51
  Assert.AreEqual(
51
52
  0,
52
53
  pointsInRange.Count,
@@ -69,11 +70,10 @@
69
70
  List<Vector2> points = new(1) { testPoint };
70
71
 
71
72
  TTree quadTree = CreateTree(points);
72
- List<Vector2> pointsInRange = quadTree
73
- .GetElementsInRange(point, range * 0.99f)
74
- .ToList();
73
+ List<Vector2> pointsInRange = new();
74
+ quadTree.GetElementsInRange(point, range * 0.99f, pointsInRange).ToList();
75
75
  Assert.AreEqual(0, pointsInRange.Count);
76
- pointsInRange = quadTree.GetElementsInRange(point, range * 1.01f).ToList();
76
+ quadTree.GetElementsInRange(point, range * 1.01f, pointsInRange);
77
77
  Assert.AreEqual(
78
78
  1,
79
79
  pointsInRange.Count,
@@ -8,7 +8,7 @@
8
8
  {
9
9
  protected override QuadTree<Vector2> CreateTree(IEnumerable<Vector2> points)
10
10
  {
11
- return new QuadTree<Vector2>(points, _ => _);
11
+ return new QuadTree<Vector2>(points, x => x);
12
12
  }
13
13
  }
14
14
  }
@@ -24,21 +24,19 @@
24
24
 
25
25
  Vector2[] points = new Vector2[1_000_000];
26
26
  float radius = 500;
27
- ParallelLoopResult result = Parallel.For(
27
+ Parallel.For(
28
28
  0,
29
- 1_000_000,
30
- index =>
29
+ 1_000,
30
+ y =>
31
31
  {
32
- // ReSharper disable once PossibleLossOfFraction
33
- points[index] = new Vector2(index % 1_000, index / 1_000);
32
+ for (int x = 0; x < 1_000; ++x)
33
+ {
34
+ int index = y * 1_000 + x;
35
+ points[index] = new Vector2(x, y);
36
+ }
34
37
  }
35
38
  );
36
39
 
37
- while (!result.IsCompleted)
38
- {
39
- yield return null;
40
- }
41
-
42
40
  Stopwatch timer = Stopwatch.StartNew();
43
41
  TTree tree = CreateTree(points);
44
42
  timer.Stop();
@@ -49,10 +47,12 @@
49
47
  int count = 0;
50
48
  TimeSpan timeout = TimeSpan.FromSeconds(1);
51
49
  timer.Restart();
50
+ List<Vector2> elementsInRange = new();
52
51
  do
53
52
  {
54
- int elementsInRange = tree.GetElementsInRange(center, radius).Count();
55
- Assert.AreEqual(785456, elementsInRange);
53
+ tree.GetElementsInRange(center, radius, elementsInRange);
54
+ int elementCount = elementsInRange.Count;
55
+ Assert.AreEqual(785456, elementCount);
56
56
  ++count;
57
57
  } while (timer.Elapsed < timeout);
58
58
 
@@ -65,8 +65,9 @@
65
65
  timer.Restart();
66
66
  do
67
67
  {
68
- int elementsInRange = tree.GetElementsInRange(center, radius).Count();
69
- Assert.AreEqual(196364, elementsInRange);
68
+ tree.GetElementsInRange(center, radius, elementsInRange);
69
+ int elementCount = elementsInRange.Count;
70
+ Assert.AreEqual(196364, elementCount);
70
71
  ++count;
71
72
  } while (timer.Elapsed < timeout);
72
73
  UnityEngine.Debug.Log(
@@ -78,8 +79,9 @@
78
79
  timer.Restart();
79
80
  do
80
81
  {
81
- int elementsInRange = tree.GetElementsInRange(center, radius).Count();
82
- Assert.AreEqual(49080, elementsInRange);
82
+ tree.GetElementsInRange(center, radius, elementsInRange);
83
+ int elementCount = elementsInRange.Count;
84
+ Assert.AreEqual(49080, elementCount);
83
85
  ++count;
84
86
  } while (timer.Elapsed < timeout);
85
87
  UnityEngine.Debug.Log(
@@ -91,8 +93,9 @@
91
93
  timer.Restart();
92
94
  do
93
95
  {
94
- int elementsInRange = tree.GetElementsInRange(center, radius).Count();
95
- Assert.AreEqual(4, elementsInRange);
96
+ tree.GetElementsInRange(center, radius, elementsInRange);
97
+ int elementCount = elementsInRange.Count;
98
+ Assert.AreEqual(4, elementCount);
96
99
  ++count;
97
100
  } while (timer.Elapsed < timeout);
98
101
  UnityEngine.Debug.Log(
@@ -149,6 +152,8 @@
149
152
  ++count;
150
153
  } while (timer.Elapsed < timeout);
151
154
  UnityEngine.Debug.Log($"| ANN - 1 | {(int)Math.Floor(count / timeout.TotalSeconds)} |");
155
+
156
+ yield break;
152
157
  }
153
158
  }
154
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.wallstop-studios.unity-helpers",
3
- "version": "2.0.0-rc76.1",
3
+ "version": "2.0.0-rc76.4",
4
4
  "displayName": "Unity Helpers",
5
5
  "description": "Various Unity Helper Library",
6
6
  "dependencies": {},
@@ -73,6 +73,9 @@
73
73
 
74
74
 
75
75
 
76
+
77
+
78
+
76
79
 
77
80
 
78
81
 
package/Editor/UI.meta DELETED
@@ -1,3 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 6b8ee675c4ab4498bbf073237de1d78b
3
- timeCreated: 1748371749