com.wallstop-studios.unity-helpers 2.0.0-rc23 → 2.0.0-rc25
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.
- package/README.md +10 -0
- package/Runtime/Core/DataStructure/CyclicBuffer.cs +35 -15
- package/Runtime/Core/DataStructure/StringWrapper.cs +1 -0
- package/Runtime/Core/Extension/IListExtensions.cs +30 -0
- package/Runtime/Core/Helper/WallMath.cs +28 -0
- package/Tests/Runtime/DataStructures/CyclicBufferTests.cs +198 -11
- package/Tests/Runtime/Helper/WallMathTests.cs +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
# A Grab-Bag
|
|
2
2
|
Various Unity Helpers. Includes many deterministic, seedable random number generators.
|
|
3
|
+
|
|
4
|
+
# To Install as Unity Package
|
|
5
|
+
1. Open Unity Package Manager
|
|
6
|
+
2. (Optional) Enable Pre-release packages to get the latest, cutting-edge builds
|
|
7
|
+
3. Open the Advanced Package Settings
|
|
8
|
+
4. Add an entry for a new "Scoped Registry"
|
|
9
|
+
- Name: `NPM`
|
|
10
|
+
- URL: `https://registry.npmjs.org`
|
|
11
|
+
- Scope(s): `com.wallstop-studios.unity-helpers`
|
|
12
|
+
5. Resolve the latest `com.wallstop-studios.unity-helpers`
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
using System.Collections;
|
|
5
5
|
using System.Collections.Generic;
|
|
6
6
|
using System.Linq;
|
|
7
|
+
using Extension;
|
|
7
8
|
using Helper;
|
|
8
9
|
|
|
9
10
|
[Serializable]
|
|
10
11
|
public sealed class CyclicBuffer<T> : IReadOnlyList<T>
|
|
11
12
|
{
|
|
13
|
+
public int Capacity { get; private set; }
|
|
12
14
|
public int Count { get; private set; }
|
|
13
|
-
public
|
|
15
|
+
public bool IsReadOnly => false;
|
|
14
16
|
|
|
15
17
|
private readonly List<T> _buffer;
|
|
16
18
|
private int _position;
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
throw new ArgumentException(nameof(capacity));
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Capacity = capacity;
|
|
40
42
|
_position = 0;
|
|
41
43
|
Count = 0;
|
|
42
44
|
_buffer = new List<T>();
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
|
|
63
65
|
public void Add(T item)
|
|
64
66
|
{
|
|
65
|
-
if (
|
|
67
|
+
if (Capacity == 0)
|
|
66
68
|
{
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
@@ -76,8 +78,8 @@
|
|
|
76
78
|
_buffer.Add(item);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
_position = _position.WrappedIncrement(
|
|
80
|
-
if (Count <
|
|
81
|
+
_position = _position.WrappedIncrement(Capacity);
|
|
82
|
+
if (Count < Capacity)
|
|
81
83
|
{
|
|
82
84
|
++Count;
|
|
83
85
|
}
|
|
@@ -88,28 +90,46 @@
|
|
|
88
90
|
/* Simply reset state */
|
|
89
91
|
Count = 0;
|
|
90
92
|
_position = 0;
|
|
93
|
+
_buffer.Clear();
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
public
|
|
96
|
+
public void Resize(int newCapacity)
|
|
94
97
|
{
|
|
95
|
-
|
|
96
|
-
if (InBounds(firstIndex))
|
|
98
|
+
if (newCapacity < 0)
|
|
97
99
|
{
|
|
98
|
-
|
|
99
|
-
return true;
|
|
100
|
+
throw new ArgumentException(nameof(newCapacity));
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
Capacity = newCapacity;
|
|
104
|
+
_buffer.Shift(-_position);
|
|
105
|
+
if (newCapacity < _buffer.Count)
|
|
106
|
+
{
|
|
107
|
+
_buffer.RemoveRange(newCapacity, _buffer.Count - newCapacity);
|
|
108
|
+
_position = 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Count = Math.Min(newCapacity, Count);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public bool Contains(T item)
|
|
115
|
+
{
|
|
116
|
+
return _buffer.Contains(item);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
private int AdjustedIndexFor(int index)
|
|
107
120
|
{
|
|
108
|
-
|
|
121
|
+
long longCapacity = Capacity;
|
|
122
|
+
if (longCapacity == 0L)
|
|
123
|
+
{
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
unchecked
|
|
109
127
|
{
|
|
110
|
-
|
|
128
|
+
int adjustedIndex = (int)(
|
|
129
|
+
(_position - 1L + longCapacity - (_buffer.Count - 1 - index)) % longCapacity
|
|
130
|
+
);
|
|
131
|
+
return adjustedIndex;
|
|
111
132
|
}
|
|
112
|
-
return (_position - 1 + capacity - index) % capacity;
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
private void BoundsCheck(int index)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Used to cache strings, meant to be used in place of strings as keys for when a Dictionary
|
|
8
8
|
has a known set of values
|
|
9
9
|
*/
|
|
10
|
+
[Serializable]
|
|
10
11
|
public sealed class StringWrapper : IEquatable<StringWrapper>, IComparable<StringWrapper>
|
|
11
12
|
{
|
|
12
13
|
private static readonly ConcurrentDictionary<string, StringWrapper> Cache = new();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
namespace UnityHelpers.Core.Extension
|
|
2
2
|
{
|
|
3
3
|
using System.Collections.Generic;
|
|
4
|
+
using Helper;
|
|
4
5
|
using Random;
|
|
5
6
|
|
|
6
7
|
public static class IListExtensions
|
|
@@ -26,6 +27,35 @@
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
public static void Shift<T>(this IList<T> list, int amount)
|
|
31
|
+
{
|
|
32
|
+
int count = list.Count;
|
|
33
|
+
if (count <= 1)
|
|
34
|
+
{
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
amount = amount.PositiveMod(count);
|
|
39
|
+
if (amount == 0)
|
|
40
|
+
{
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Reverse(list, 0, count - 1);
|
|
45
|
+
Reverse(list, 0, amount - 1);
|
|
46
|
+
Reverse(list, amount, count - 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static void Reverse<T>(this IList<T> list, int start, int end)
|
|
50
|
+
{
|
|
51
|
+
while (start < end)
|
|
52
|
+
{
|
|
53
|
+
(list[start], list[end]) = (list[end], list[start]);
|
|
54
|
+
start++;
|
|
55
|
+
end--;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
29
59
|
public static void RemoveAtSwapBack<T>(this IList<T> list, int index)
|
|
30
60
|
{
|
|
31
61
|
if (list.Count <= 1)
|
|
@@ -31,6 +31,34 @@
|
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
public static float PositiveMod(this float value, float max)
|
|
35
|
+
{
|
|
36
|
+
value %= max;
|
|
37
|
+
value += max;
|
|
38
|
+
return value % max;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public static double PositiveMod(this double value, double max)
|
|
42
|
+
{
|
|
43
|
+
value %= max;
|
|
44
|
+
value += max;
|
|
45
|
+
return value % max;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static int PositiveMod(this int value, int max)
|
|
49
|
+
{
|
|
50
|
+
value %= max;
|
|
51
|
+
value += max;
|
|
52
|
+
return value % max;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public static long PositiveMod(this long value, long max)
|
|
56
|
+
{
|
|
57
|
+
value %= max;
|
|
58
|
+
value += max;
|
|
59
|
+
return value % max;
|
|
60
|
+
}
|
|
61
|
+
|
|
34
62
|
public static int WrappedAdd(this int value, int increment, int max)
|
|
35
63
|
{
|
|
36
64
|
WrappedAdd(ref value, increment, max);
|
|
@@ -25,6 +25,28 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
[Test]
|
|
29
|
+
public void CapacityInitializedOk()
|
|
30
|
+
{
|
|
31
|
+
for (int i = 0; i < NumTries; i++)
|
|
32
|
+
{
|
|
33
|
+
int capacity = PRNG.Instance.Next(1, int.MaxValue);
|
|
34
|
+
CyclicBuffer<int> buffer = new(capacity);
|
|
35
|
+
Assert.AreEqual(capacity, buffer.Capacity);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[Test]
|
|
40
|
+
public void CountInitializedOk()
|
|
41
|
+
{
|
|
42
|
+
for (int i = 0; i < NumTries; i++)
|
|
43
|
+
{
|
|
44
|
+
int capacity = PRNG.Instance.Next(1, int.MaxValue);
|
|
45
|
+
CyclicBuffer<int> buffer = new(capacity);
|
|
46
|
+
Assert.AreEqual(0, buffer.Count);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
[Test]
|
|
29
51
|
public void ZeroCapacityOk()
|
|
30
52
|
{
|
|
@@ -49,7 +71,14 @@
|
|
|
49
71
|
int value = PRNG.Instance.Next();
|
|
50
72
|
buffer.Add(value);
|
|
51
73
|
expected.Add(value);
|
|
52
|
-
|
|
74
|
+
if (!expected.SequenceEqual(buffer))
|
|
75
|
+
{
|
|
76
|
+
Assert.Fail(
|
|
77
|
+
$"Failure at iteration {i}, capacity={buffer.Capacity}, "
|
|
78
|
+
+ $"capacityMultiplier={CapacityMultiplier}\n"
|
|
79
|
+
+ $"Expected: [{string.Join(",", expected)}], Actual: [{string.Join(",", buffer)}]"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
53
82
|
}
|
|
54
83
|
}
|
|
55
84
|
|
|
@@ -68,10 +97,58 @@
|
|
|
68
97
|
}
|
|
69
98
|
}
|
|
70
99
|
|
|
100
|
+
[Test]
|
|
101
|
+
public void InitialElementsVariableSize()
|
|
102
|
+
{
|
|
103
|
+
for (int i = 0; i < NumTries; ++i)
|
|
104
|
+
{
|
|
105
|
+
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
106
|
+
int[] elements = Enumerable
|
|
107
|
+
.Range(0, (int)(capacity * PRNG.Instance.NextFloat(0.5f, 1.5f)))
|
|
108
|
+
.Select(_ => PRNG.Instance.Next())
|
|
109
|
+
.ToArray();
|
|
110
|
+
CyclicBuffer<int> buffer = new(capacity, elements);
|
|
111
|
+
if (capacity < elements.Length)
|
|
112
|
+
{
|
|
113
|
+
Assert.IsTrue(elements.Skip(elements.Length - capacity).SequenceEqual(buffer));
|
|
114
|
+
}
|
|
115
|
+
else
|
|
116
|
+
{
|
|
117
|
+
Assert.IsTrue(elements.SequenceEqual(buffer));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[Test]
|
|
123
|
+
public void InitialElementsSizeSameAsCapacity()
|
|
124
|
+
{
|
|
125
|
+
for (int i = 0; i < NumTries; ++i)
|
|
126
|
+
{
|
|
127
|
+
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
128
|
+
int[] elements = Enumerable
|
|
129
|
+
.Range(0, capacity)
|
|
130
|
+
.Select(_ => PRNG.Instance.Next())
|
|
131
|
+
.ToArray();
|
|
132
|
+
CyclicBuffer<int> buffer = new(capacity, elements);
|
|
133
|
+
Assert.IsTrue(elements.SequenceEqual(buffer));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
[Test]
|
|
138
|
+
public void InitialElementsEmptyInput()
|
|
139
|
+
{
|
|
140
|
+
for (int i = 0; i < NumTries; ++i)
|
|
141
|
+
{
|
|
142
|
+
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
143
|
+
CyclicBuffer<int> buffer = new(capacity, Array.Empty<int>());
|
|
144
|
+
Assert.IsTrue(Array.Empty<int>().SequenceEqual(buffer));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
71
148
|
[Test]
|
|
72
149
|
public void NormalAndWrappingBehavior()
|
|
73
150
|
{
|
|
74
|
-
|
|
151
|
+
LinkedList<int> expected = new();
|
|
75
152
|
for (int i = 0; i < NumTries; ++i)
|
|
76
153
|
{
|
|
77
154
|
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
@@ -81,20 +158,130 @@
|
|
|
81
158
|
for (int j = 0; j < capacity * CapacityMultiplier; ++j)
|
|
82
159
|
{
|
|
83
160
|
int newValue = PRNG.Instance.Next();
|
|
84
|
-
|
|
161
|
+
expected.AddLast(newValue);
|
|
162
|
+
while (capacity < expected.Count)
|
|
85
163
|
{
|
|
86
|
-
expected
|
|
164
|
+
expected.RemoveFirst();
|
|
87
165
|
}
|
|
88
|
-
|
|
166
|
+
buffer.Add(newValue);
|
|
167
|
+
Assert.AreEqual(expected.Count, buffer.Count);
|
|
168
|
+
if (!expected.SequenceEqual(buffer))
|
|
89
169
|
{
|
|
90
|
-
|
|
170
|
+
Assert.Fail(
|
|
171
|
+
$"Failure at iteration {i}, j={j}, capacity={buffer.Capacity}, "
|
|
172
|
+
+ $"capacityMultiplier={CapacityMultiplier}\n"
|
|
173
|
+
+ $"Expected: [{string.Join(",", expected)}], Actual: [{string.Join(",", buffer)}]"
|
|
174
|
+
);
|
|
91
175
|
}
|
|
92
|
-
buffer.Add(newValue);
|
|
93
|
-
Assert.IsTrue(
|
|
94
|
-
expected.SequenceEqual(buffer),
|
|
95
|
-
$"Failure at iteration {i}, j={j}, capacity={capacity}, capacityMultiplier={CapacityMultiplier}"
|
|
96
|
-
);
|
|
97
176
|
}
|
|
177
|
+
|
|
178
|
+
foreach (int item in expected)
|
|
179
|
+
{
|
|
180
|
+
Assert.IsTrue(buffer.Contains(item));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (int j = 0; j < NumTries; ++j)
|
|
184
|
+
{
|
|
185
|
+
Assert.IsFalse(buffer.Contains(PRNG.Instance.Next(int.MinValue, -1)));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
[Test]
|
|
191
|
+
public void ClearOk()
|
|
192
|
+
{
|
|
193
|
+
HashSet<int> seen = new();
|
|
194
|
+
for (int i = 0; i < NumTries; ++i)
|
|
195
|
+
{
|
|
196
|
+
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
197
|
+
CyclicBuffer<int> buffer = new(capacity);
|
|
198
|
+
float fillPercent = PRNG.Instance.NextFloat(0.5f, 1.5f);
|
|
199
|
+
seen.Clear();
|
|
200
|
+
for (int j = 0; j < capacity * fillPercent; ++j)
|
|
201
|
+
{
|
|
202
|
+
int value = PRNG.Instance.Next();
|
|
203
|
+
seen.Add(value);
|
|
204
|
+
buffer.Add(value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
Assert.AreNotEqual(0, buffer.Count);
|
|
208
|
+
Assert.IsFalse(Array.Empty<int>().SequenceEqual(buffer));
|
|
209
|
+
buffer.Clear();
|
|
210
|
+
|
|
211
|
+
Assert.AreEqual(0, buffer.Count);
|
|
212
|
+
Assert.AreEqual(capacity, buffer.Capacity);
|
|
213
|
+
Assert.IsTrue(Array.Empty<int>().SequenceEqual(buffer));
|
|
214
|
+
|
|
215
|
+
// Make sure our data is actually cleaned up, none of our input data should be "Contained"
|
|
216
|
+
foreach (int value in seen)
|
|
217
|
+
{
|
|
218
|
+
Assert.IsFalse(buffer.Contains(value));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
[Test]
|
|
224
|
+
public void ResizeFullOk()
|
|
225
|
+
{
|
|
226
|
+
for (int i = 0; i < NumTries; ++i)
|
|
227
|
+
{
|
|
228
|
+
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
229
|
+
CyclicBuffer<int> buffer = new(capacity);
|
|
230
|
+
float fillPercent = PRNG.Instance.NextFloat(1f, 2f);
|
|
231
|
+
float capacityPercent = PRNG.Instance.NextFloat(0.3f, 0.9f);
|
|
232
|
+
for (int j = 0; j < capacity * fillPercent; ++j)
|
|
233
|
+
{
|
|
234
|
+
int value = PRNG.Instance.Next();
|
|
235
|
+
buffer.Add(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
int[] values = buffer.ToArray();
|
|
239
|
+
|
|
240
|
+
int newCapacity = Math.Max(0, (int)(capacity * capacityPercent));
|
|
241
|
+
buffer.Resize(newCapacity);
|
|
242
|
+
int[] newValues = buffer.ToArray();
|
|
243
|
+
Assert.AreEqual(newCapacity, buffer.Capacity);
|
|
244
|
+
Assert.AreEqual(newCapacity, newValues.Length);
|
|
245
|
+
Assert.That(values.Take(newCapacity), Is.EqualTo(newValues));
|
|
246
|
+
|
|
247
|
+
newCapacity = 0;
|
|
248
|
+
buffer.Resize(newCapacity);
|
|
249
|
+
newValues = buffer.ToArray();
|
|
250
|
+
Assert.AreEqual(newCapacity, buffer.Capacity);
|
|
251
|
+
Assert.AreEqual(newCapacity, newValues.Length);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
[Test]
|
|
256
|
+
public void ResizePartialOk()
|
|
257
|
+
{
|
|
258
|
+
for (int i = 0; i < NumTries; ++i)
|
|
259
|
+
{
|
|
260
|
+
int capacity = PRNG.Instance.Next(100, 1_000);
|
|
261
|
+
CyclicBuffer<int> buffer = new(capacity);
|
|
262
|
+
float fillPercent = PRNG.Instance.NextFloat(0.3f, 0.9f);
|
|
263
|
+
float capacityPercent = PRNG.Instance.NextFloat(0.3f, 0.9f);
|
|
264
|
+
int filled = (int)(capacity * fillPercent);
|
|
265
|
+
for (int j = 0; j < filled; ++j)
|
|
266
|
+
{
|
|
267
|
+
int value = PRNG.Instance.Next();
|
|
268
|
+
buffer.Add(value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
int[] values = buffer.ToArray();
|
|
272
|
+
|
|
273
|
+
int newCapacity = Math.Max(0, (int)(capacity * capacityPercent));
|
|
274
|
+
buffer.Resize(newCapacity);
|
|
275
|
+
int[] newValues = buffer.ToArray();
|
|
276
|
+
Assert.AreEqual(newCapacity, buffer.Capacity);
|
|
277
|
+
Assert.AreEqual(Math.Min(filled, newCapacity), newValues.Length);
|
|
278
|
+
Assert.That(values.Take(newCapacity), Is.EqualTo(newValues));
|
|
279
|
+
|
|
280
|
+
newCapacity = 0;
|
|
281
|
+
buffer.Resize(newCapacity);
|
|
282
|
+
newValues = buffer.ToArray();
|
|
283
|
+
Assert.AreEqual(newCapacity, buffer.Capacity);
|
|
284
|
+
Assert.AreEqual(newCapacity, newValues.Length);
|
|
98
285
|
}
|
|
99
286
|
}
|
|
100
287
|
}
|
|
@@ -44,6 +44,19 @@
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
[Test]
|
|
48
|
+
public void PositiveMod()
|
|
49
|
+
{
|
|
50
|
+
Assert.AreEqual(9, (-1).PositiveMod(10));
|
|
51
|
+
Assert.AreEqual(1, 1.PositiveMod(10));
|
|
52
|
+
Assert.AreEqual(9f, (-1f).PositiveMod(10f));
|
|
53
|
+
Assert.AreEqual(1f, 1f.PositiveMod(10f));
|
|
54
|
+
Assert.AreEqual(9.0, (-1.0).PositiveMod(10.0));
|
|
55
|
+
Assert.AreEqual(1.0, 1.0.PositiveMod(10.0));
|
|
56
|
+
Assert.AreEqual(9L, (-1L).PositiveMod(10L));
|
|
57
|
+
Assert.AreEqual(1L, 1L.PositiveMod(10L));
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
[Test]
|
|
48
61
|
public void ApproximatelyExpected()
|
|
49
62
|
{
|
|
@@ -51,7 +64,6 @@
|
|
|
51
64
|
Assert.IsTrue(0f.Approximately(0.5f, 1f));
|
|
52
65
|
Assert.IsFalse(0.001f.Approximately(0f, 0f));
|
|
53
66
|
Assert.IsFalse(100f.Approximately(5f, 2.4f));
|
|
54
|
-
|
|
55
67
|
Assert.IsTrue(0.001f.Approximately(0.0001f));
|
|
56
68
|
}
|
|
57
69
|
|