21
21
package me .lucko .spark .common .platform .world ;
22
22
23
23
import me .lucko .spark .proto .SparkProtos .WorldStatistics ;
24
+ import org .jetbrains .annotations .VisibleForTesting ;
24
25
26
+ import java .util .ArrayDeque ;
25
27
import java .util .ArrayList ;
26
28
import java .util .Collection ;
27
29
import java .util .HashMap ;
28
30
import java .util .HashSet ;
29
- import java .util .Iterator ;
31
+ import java .util .LinkedHashMap ;
30
32
import java .util .List ;
33
+ import java .util .Map ;
31
34
import java .util .Set ;
32
35
import java .util .concurrent .atomic .AtomicInteger ;
33
36
@@ -122,37 +125,57 @@ private static <E> WorldStatistics.Chunk chunkToProto(ChunkInfo<E> chunk, CountM
122
125
return builder .build ();
123
126
}
124
127
125
- private static List <Region > groupIntoRegions (List <? extends ChunkInfo <?>> chunks ) {
128
+ @ VisibleForTesting
129
+ static List <Region > groupIntoRegions (List <? extends ChunkInfo <?>> chunks ) {
126
130
List <Region > regions = new ArrayList <>();
127
131
132
+ LinkedHashMap <ChunkCoordinate , ChunkInfo <?>> chunkMap = new LinkedHashMap <>(chunks .size ());
133
+
128
134
for (ChunkInfo <?> chunk : chunks ) {
129
135
CountMap <?> counts = chunk .getEntityCounts ();
130
136
if (counts .total ().get () == 0 ) {
131
137
continue ;
132
138
}
139
+ chunkMap .put (new ChunkCoordinate (chunk .getX (), chunk .getZ ()), chunk );
140
+ }
133
141
134
- boolean found = false ;
142
+ ArrayDeque <ChunkInfo <?>> queue = new ArrayDeque <>();
143
+ ChunkCoordinate index = new ChunkCoordinate (); // avoid allocating per check
135
144
136
- for (Region region : regions ) {
137
- if (region .isAdjacent (chunk )) {
138
- found = true ;
139
- region .add (chunk );
140
-
141
- // if the chunk is adjacent to more than one region, merge the regions together
142
- for (Iterator <Region > iterator = regions .iterator (); iterator .hasNext (); ) {
143
- Region otherRegion = iterator .next ();
144
- if (region != otherRegion && otherRegion .isAdjacent (chunk )) {
145
- iterator .remove ();
146
- region .merge (otherRegion );
145
+ while (!chunkMap .isEmpty ()) {
146
+ Map .Entry <ChunkCoordinate , ChunkInfo <?>> first = chunkMap .entrySet ().iterator ().next ();
147
+ ChunkInfo <?> firstValue = first .getValue ();
148
+
149
+ chunkMap .remove (first .getKey ());
150
+
151
+ Region region = new Region (firstValue );
152
+ regions .add (region );
153
+
154
+ queue .add (firstValue );
155
+
156
+ ChunkInfo <?> queued ;
157
+ while ((queued = queue .pollFirst ()) != null ) {
158
+ int queuedX = queued .getX ();
159
+ int queuedZ = queued .getZ ();
160
+
161
+ // merge adjacent chunks
162
+ for (int dz = -1 ; dz <= 1 ; ++dz ) {
163
+ for (int dx = -1 ; dx <= 1 ; ++dx ) {
164
+ if ((dx | dz ) == 0 ) {
165
+ continue ;
147
166
}
148
- }
149
167
150
- break ;
151
- }
152
- }
168
+ index .setCoordinate (queuedX + dx , queuedZ + dz );
169
+ ChunkInfo <?> adjacent = chunkMap .remove (index );
170
+
171
+ if (adjacent == null ) {
172
+ continue ;
173
+ }
153
174
154
- if (!found ) {
155
- regions .add (new Region (chunk ));
175
+ region .add (adjacent );
176
+ queue .add (adjacent );
177
+ }
178
+ }
156
179
}
157
180
}
158
181
@@ -162,8 +185,7 @@ private static List<Region> groupIntoRegions(List<? extends ChunkInfo<?>> chunks
162
185
/**
163
186
* A map of nearby chunks grouped together by Euclidean distance.
164
187
*/
165
- private static final class Region {
166
- private static final int DISTANCE_THRESHOLD = 2 ;
188
+ static final class Region {
167
189
private final Set <ChunkInfo <?>> chunks ;
168
190
private final AtomicInteger totalEntities ;
169
191
@@ -181,30 +203,53 @@ public AtomicInteger getTotalEntities() {
181
203
return this .totalEntities ;
182
204
}
183
205
184
- public boolean isAdjacent (ChunkInfo <?> chunk ) {
185
- for (ChunkInfo <?> el : this .chunks ) {
186
- if (squaredEuclideanDistance (el , chunk ) <= DISTANCE_THRESHOLD ) {
187
- return true ;
188
- }
189
- }
190
- return false ;
191
- }
192
-
193
206
public void add (ChunkInfo <?> chunk ) {
194
207
this .chunks .add (chunk );
195
208
this .totalEntities .addAndGet (chunk .getEntityCounts ().total ().get ());
196
209
}
210
+ }
211
+
212
+ static final class ChunkCoordinate implements Comparable <ChunkCoordinate > {
213
+ long key ;
214
+
215
+ ChunkCoordinate () {}
197
216
198
- public void merge (Region group ) {
199
- this .chunks .addAll (group .getChunks ());
200
- this .totalEntities .addAndGet (group .getTotalEntities ().get ());
217
+ ChunkCoordinate (int chunkX , int chunkZ ) {
218
+ this .setCoordinate (chunkX , chunkZ );
201
219
}
202
220
203
- private static long squaredEuclideanDistance (ChunkInfo <?> a , ChunkInfo <?> b ) {
204
- long dx = a .getX () - b .getX ();
205
- long dz = a .getZ () - b .getZ ();
206
- return (dx * dx ) + (dz * dz );
221
+ ChunkCoordinate (long key ) {
222
+ this .setKey (key );
223
+ }
224
+
225
+ public void setCoordinate (int chunkX , int chunkZ ) {
226
+ this .setKey (((long ) chunkZ << 32 ) | (chunkX & 0xFFFFFFFFL ));
207
227
}
208
- }
209
228
229
+ public void setKey (long key ) {
230
+ this .key = key ;
231
+ }
232
+
233
+ @ Override
234
+ public int hashCode () {
235
+ // fastutil hash without the last step, as it is done by HashMap
236
+ // doing the last step twice (h ^= (h >>> 16)) is both more expensive and destroys the hash
237
+ long h = this .key * 0x9E3779B97F4A7C15L ;
238
+ h ^= h >>> 32 ;
239
+ return (int ) h ;
240
+ }
241
+
242
+ @ Override
243
+ public boolean equals (Object obj ) {
244
+ if (!(obj instanceof ChunkCoordinate )) {
245
+ return false ;
246
+ }
247
+ return this .key == ((ChunkCoordinate ) obj ).key ;
248
+ }
249
+
250
+ @ Override
251
+ public int compareTo (ChunkCoordinate other ) {
252
+ return Long .compare (this .key , other .key );
253
+ }
254
+ }
210
255
}
0 commit comments