progress
This commit is contained in:
@@ -62,6 +62,19 @@ public class ControlPathfinder{
|
|||||||
((PathTile.team(tile) != team && PathTile.team(tile) != 0) && PathTile.solid(tile) ? wallImpassableCap : 0) +
|
((PathTile.team(tile) != team && PathTile.team(tile) != 0) && PathTile.solid(tile) ? wallImpassableCap : 0) +
|
||||||
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 6 : 0);
|
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 6 : 0);
|
||||||
|
|
||||||
|
public static final int
|
||||||
|
costIdGround = 0,
|
||||||
|
costIdHover = 1,
|
||||||
|
costIdLegs = 2,
|
||||||
|
costIdNaval = 3;
|
||||||
|
|
||||||
|
public static final Seq<PathCost> costTypes = Seq.with(
|
||||||
|
costGround,
|
||||||
|
costHover,
|
||||||
|
costLegs,
|
||||||
|
costNaval
|
||||||
|
);
|
||||||
|
|
||||||
public static boolean showDebug = false;
|
public static boolean showDebug = false;
|
||||||
|
|
||||||
//static access probably faster than object access
|
//static access probably faster than object access
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
0, -1
|
0, -1
|
||||||
};
|
};
|
||||||
|
|
||||||
//maps pathCost -> flattened array of clusters in 2D
|
//maps team -> pathCost -> flattened array of clusters in 2D
|
||||||
//(what about teams? different path costs?)
|
//(what about teams? different path costs?)
|
||||||
Cluster[][] clusters;
|
Cluster[][][] clusters;
|
||||||
|
|
||||||
int cwidth, cheight;
|
int cwidth, cheight;
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
//these contain
|
//these contain
|
||||||
static class PathRequest{
|
static class PathRequest{
|
||||||
final Unit unit;
|
final Unit unit;
|
||||||
final int destination;
|
final int destination, team;
|
||||||
//resulting path of nodes
|
//resulting path of nodes
|
||||||
final IntSeq resultPath = new IntSeq();
|
final IntSeq resultPath = new IntSeq();
|
||||||
//node index -> total cost
|
//node index -> total cost
|
||||||
@@ -93,10 +93,11 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
final PathfindQueue frontier = new PathfindQueue();
|
final PathfindQueue frontier = new PathfindQueue();
|
||||||
|
|
||||||
//main thread only!
|
//main thread only!
|
||||||
long lastUpdateId;
|
long lastUpdateId = state.updateId;
|
||||||
|
|
||||||
public PathRequest(Unit unit, int destination){
|
public PathRequest(Unit unit, int team, int destination){
|
||||||
this.unit = unit;
|
this.unit = unit;
|
||||||
|
this.team = team;
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +130,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
stop();
|
stop();
|
||||||
|
|
||||||
//TODO 5 path costs, arbitrary number
|
//TODO 5 path costs, arbitrary number
|
||||||
clusters = new Cluster[5][];
|
clusters = new Cluster[256][][];
|
||||||
cwidth = Mathf.ceil((float)world.width() / clusterSize);
|
cwidth = Mathf.ceil((float)world.width() / clusterSize);
|
||||||
cheight = Mathf.ceil((float)world.height() / clusterSize);
|
cheight = Mathf.ceil((float)world.height() / clusterSize);
|
||||||
|
|
||||||
@@ -144,7 +145,16 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
|
|
||||||
//is at the edge of a cluster; this means the portals may have changed.
|
//is at the edge of a cluster; this means the portals may have changed.
|
||||||
if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){
|
if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){
|
||||||
queue.post(() -> clustersToUpdate.add(cluster));
|
|
||||||
|
|
||||||
|
if(mx == 0) queueClusterUpdate(cx - 1, cy); //left
|
||||||
|
if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom
|
||||||
|
if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right
|
||||||
|
if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top
|
||||||
|
|
||||||
|
|
||||||
|
queueClusterUpdate(cx, cy);
|
||||||
|
//TODO: recompute edge clusters too.
|
||||||
}else{
|
}else{
|
||||||
//there is no need to recompute portals for block updates that are not on the edge.
|
//there is no need to recompute portals for block updates that are not on the edge.
|
||||||
queue.post(() -> clustersToInnerUpdate.add(cluster));
|
queue.post(() -> clustersToInnerUpdate.add(cluster));
|
||||||
@@ -169,7 +179,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(debug){
|
if(debug && false){
|
||||||
Events.run(Trigger.draw, () -> {
|
Events.run(Trigger.draw, () -> {
|
||||||
int team = Team.sharded.id;
|
int team = Team.sharded.id;
|
||||||
int cost = costGround;
|
int cost = costGround;
|
||||||
@@ -180,7 +190,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
Lines.stroke(1f);
|
Lines.stroke(1f);
|
||||||
for(int cx = 0; cx < cwidth; cx++){
|
for(int cx = 0; cx < cwidth; cx++){
|
||||||
for(int cy = 0; cy < cheight; cy++){
|
for(int cy = 0; cy < cheight; cy++){
|
||||||
var cluster = clusters[cost][cy * cwidth + cx];
|
var cluster = clusters[Team.sharded.id][cost][cy * cwidth + cx];
|
||||||
if(cluster != null){
|
if(cluster != null){
|
||||||
Lines.stroke(0.5f);
|
Lines.stroke(0.5f);
|
||||||
Draw.color(Color.gray);
|
Draw.color(Color.gray);
|
||||||
@@ -264,6 +274,35 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void queueClusterUpdate(int cx, int cy){
|
||||||
|
if(cx >= 0 && cy >= 0 && cx < cwidth && cy < cheight){
|
||||||
|
queue.post(() -> clustersToUpdate.add(cx + cy * cwidth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//DEBUGGING ONLY
|
||||||
|
Vec2 nodeToVec(int current, Vec2 out){
|
||||||
|
portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){
|
||||||
|
portalToVec(clusters[Team.sharded.id][pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){
|
||||||
|
int pos = cluster.portals[direction].items[portalIndex];
|
||||||
|
int from = Point2.x(pos), to = Point2.y(pos);
|
||||||
|
int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1];
|
||||||
|
float average = (from + to) / 2f;
|
||||||
|
|
||||||
|
float
|
||||||
|
x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize,
|
||||||
|
y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize;
|
||||||
|
|
||||||
|
out.set(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
/** Starts or restarts the pathfinding thread. */
|
/** Starts or restarts the pathfinding thread. */
|
||||||
private void start(){
|
private void start(){
|
||||||
stop();
|
stop();
|
||||||
@@ -284,34 +323,56 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
queue.clear();
|
queue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec2 nodeToVec(int current, Vec2 out){
|
/** @return a cluster at coordinates; can be null if not cluster was created yet*/
|
||||||
portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out);
|
@Nullable Cluster getCluster(int team, int pathCost, int cx, int cy){
|
||||||
return out;
|
return getCluster(team, pathCost, cx + cy * cwidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){
|
/** @return a cluster at coordinates; can be null if not cluster was created yet*/
|
||||||
portalToVec(clusters[pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out);
|
@Nullable Cluster getCluster(int team, int pathCost, int clusterIndex){
|
||||||
|
Cluster[][] dim1 = clusters[team];
|
||||||
|
|
||||||
|
if(dim1 == null) return null;
|
||||||
|
|
||||||
|
Cluster[] dim2 = dim1[pathCost];
|
||||||
|
|
||||||
|
if(dim2 == null) return null;
|
||||||
|
|
||||||
|
return dim2[clusterIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){
|
/** @return the cluster at specified coordinates; never null. */
|
||||||
int pos = cluster.portals[direction].items[portalIndex];
|
Cluster getCreateCluster(int team, int pathCost, int cx, int cy){
|
||||||
int from = Point2.x(pos), to = Point2.y(pos);
|
return getCreateCluster(team, pathCost, cx + cy * cwidth);
|
||||||
int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1];
|
}
|
||||||
float average = (from + to) / 2f;
|
|
||||||
|
|
||||||
float
|
/** @return the cluster at specified coordinates; never null. */
|
||||||
x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize,
|
Cluster getCreateCluster(int team, int pathCost, int clusterIndex){
|
||||||
y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize;
|
Cluster result = getCluster(team, pathCost, clusterIndex);
|
||||||
|
if(result == null){
|
||||||
out.set(x, y);
|
return createCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth);
|
||||||
|
}else{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: this is never called yet. should be invoked during pathfinding
|
//TODO: this is never called yet. should be invoked during pathfinding
|
||||||
void createCluster(int team, int pathCost, int cx, int cy){
|
Cluster createCluster(int team, int pathCost, int cx, int cy){
|
||||||
if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight];
|
Cluster[][] dim1 = clusters[team];
|
||||||
Cluster cluster = clusters[pathCost][cy * cwidth + cx];
|
|
||||||
|
if(dim1 == null){
|
||||||
|
dim1 = clusters[team] = new Cluster[Team.all.length][];
|
||||||
|
}
|
||||||
|
|
||||||
|
Cluster[] dim2 = dim1[pathCost];
|
||||||
|
|
||||||
|
if(dim2 == null){
|
||||||
|
dim2 = dim1[pathCost] = new Cluster[cwidth * cheight];
|
||||||
|
}
|
||||||
|
|
||||||
|
Cluster cluster = dim2[cy * cwidth + cx];
|
||||||
if(cluster == null){
|
if(cluster == null){
|
||||||
cluster = clusters[pathCost][cy * cwidth + cx] = new Cluster();
|
cluster = dim2[cy * cwidth + cx] = new Cluster();
|
||||||
}else{
|
}else{
|
||||||
//reset data
|
//reset data
|
||||||
for(var p : cluster.portals){
|
for(var p : cluster.portals){
|
||||||
@@ -334,7 +395,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Cluster other = clusters[pathCost][otherX + otherY * cwidth];
|
Cluster other = dim2[otherX + otherY * cwidth];
|
||||||
IntSeq portals;
|
IntSeq portals;
|
||||||
|
|
||||||
if(other == null){
|
if(other == null){
|
||||||
@@ -388,6 +449,8 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectInnerEdges(cx, cy, team, cost, cluster);
|
connectInnerEdges(cx, cy, team, cost, cluster);
|
||||||
|
|
||||||
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){
|
void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){
|
||||||
@@ -563,7 +626,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
PathCost cost = ControlPathfinder.costGround;
|
PathCost cost = ControlPathfinder.costGround;
|
||||||
|
|
||||||
//TODO: cluster can be null!!
|
//TODO: cluster can be null!!
|
||||||
Cluster cluster = clusters[pathCost][cx + cy * cwidth];
|
Cluster cluster = getCreateCluster(team, pathCost, cx, cy);
|
||||||
int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1);
|
int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1);
|
||||||
|
|
||||||
int bestPortalPair = Integer.MAX_VALUE;
|
int bestPortalPair = Integer.MAX_VALUE;
|
||||||
@@ -616,7 +679,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
//distance heuristic: manhattan
|
//distance heuristic: manhattan
|
||||||
private float clusterNodeHeuristic(int pathCost, int nodeA, int nodeB){
|
private float clusterNodeHeuristic(int team, int pathCost, int nodeA, int nodeB){
|
||||||
int
|
int
|
||||||
clusterA = NodeIndex.cluster(nodeA),
|
clusterA = NodeIndex.cluster(nodeA),
|
||||||
dirA = NodeIndex.dir(nodeA),
|
dirA = NodeIndex.dir(nodeA),
|
||||||
@@ -624,8 +687,8 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
clusterB = NodeIndex.cluster(nodeB),
|
clusterB = NodeIndex.cluster(nodeB),
|
||||||
dirB = NodeIndex.dir(nodeB),
|
dirB = NodeIndex.dir(nodeB),
|
||||||
portalB = NodeIndex.portal(nodeB),
|
portalB = NodeIndex.portal(nodeB),
|
||||||
rangeA = clusters[pathCost][clusterA].portals[dirA].items[portalA],
|
rangeA = getCreateCluster(team, pathCost, clusterA).portals[dirA].items[portalA],
|
||||||
rangeB = clusters[pathCost][clusterB].portals[dirB].items[portalB];
|
rangeB = getCreateCluster(team, pathCost, clusterB).portals[dirB].items[portalB];
|
||||||
|
|
||||||
float
|
float
|
||||||
averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f,
|
averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f,
|
||||||
@@ -652,6 +715,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
var costs = request.costs;
|
var costs = request.costs;
|
||||||
var cameFrom = request.cameFrom;
|
var cameFrom = request.cameFrom;
|
||||||
var frontier = request.frontier;
|
var frontier = request.frontier;
|
||||||
|
var team = request.team;
|
||||||
|
|
||||||
frontier.clear();
|
frontier.clear();
|
||||||
costs.clear();
|
costs.clear();
|
||||||
@@ -673,23 +737,22 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
|
|
||||||
int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current);
|
int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current);
|
||||||
int cx = cluster % cwidth, cy = cluster / cwidth;
|
int cx = cluster % cwidth, cy = cluster / cwidth;
|
||||||
Cluster clust = clusters[pathCost][cluster];
|
Cluster clust = getCreateCluster(team, pathCost, cluster);
|
||||||
LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal];
|
LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal];
|
||||||
|
|
||||||
//edges for the cluster the node is 'in'
|
//edges for the cluster the node is 'in'
|
||||||
if(innerCons != null){
|
if(innerCons != null){
|
||||||
checkEdges(request, pathCost, current, endNodeIndex, cx, cy, innerCons);
|
checkEdges(request, team, pathCost, current, endNodeIndex, cx, cy, innerCons);
|
||||||
}
|
}
|
||||||
|
|
||||||
//edges that this node 'faces' from the other side
|
//edges that this node 'faces' from the other side
|
||||||
int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y;
|
int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y;
|
||||||
if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){
|
if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){
|
||||||
int nextClusteri = nextCx + nextCy * cwidth;
|
Cluster nextCluster = getCreateCluster(team, pathCost, nextCx, nextCy);
|
||||||
Cluster nextCluster = clusters[pathCost][nextClusteri];
|
|
||||||
int relativeDir = (dir + 2) % 4;
|
int relativeDir = (dir + 2) % 4;
|
||||||
LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal];
|
LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal];
|
||||||
if(outerCons != null){
|
if(outerCons != null){
|
||||||
checkEdges(request, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons);
|
checkEdges(request, team, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,7 +781,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()});
|
Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()});
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkEdges(PathRequest request, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){
|
void checkEdges(PathRequest request, int team, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){
|
||||||
for(int i = 0; i < connections.size; i++){
|
for(int i = 0; i < connections.size; i++){
|
||||||
long con = connections.items[i];
|
long con = connections.items[i];
|
||||||
float cost = IntraEdge.cost(con);
|
float cost = IntraEdge.cost(con);
|
||||||
@@ -730,7 +793,7 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
if(newCost < request.costs.get(next, Float.POSITIVE_INFINITY)){
|
if(newCost < request.costs.get(next, Float.POSITIVE_INFINITY)){
|
||||||
request.costs.put(next, newCost);
|
request.costs.put(next, newCost);
|
||||||
|
|
||||||
request.frontier.add(next, newCost + clusterNodeHeuristic(pathCost, next, goal));
|
request.frontier.add(next, newCost + clusterNodeHeuristic(team, pathCost, next, goal));
|
||||||
request.cameFrom.put(next, current);
|
request.cameFrom.put(next, current);
|
||||||
|
|
||||||
//TODO debug
|
//TODO debug
|
||||||
@@ -881,11 +944,10 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
destY = World.toTile(destination.y) * wwidth,
|
destY = World.toTile(destination.y) * wwidth,
|
||||||
destPos = destX + destY * wwidth;
|
destPos = destX + destY * wwidth;
|
||||||
|
|
||||||
//TODO: collect old requests that have not been accessed in a while. not sure where.
|
|
||||||
request.lastUpdateId = state.updateId;
|
|
||||||
|
|
||||||
//use existing request if it exists.
|
//use existing request if it exists.
|
||||||
if(request != null && request.destination == destPos){
|
if(request != null && request.destination == destPos){
|
||||||
|
//TODO: collect old requests that have not been accessed in a while. not sure where.
|
||||||
|
request.lastUpdateId = state.updateId;
|
||||||
|
|
||||||
Tile tileOn = unit.tileOn();
|
Tile tileOn = unit.tileOn();
|
||||||
//TODO: should fields be accessible from this thread?
|
//TODO: should fields be accessible from this thread?
|
||||||
@@ -923,8 +985,9 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
|
|
||||||
//queue new request.
|
//queue new request.
|
||||||
unitRequests.put(unit, request = new PathRequest(unit, destPos));
|
unitRequests.put(unit, request = new PathRequest(unit, unit.team.id, destPos));
|
||||||
|
|
||||||
PathRequest f = request;
|
PathRequest f = request;
|
||||||
|
|
||||||
@@ -980,8 +1043,10 @@ public class HierarchyPathFinder implements Runnable{
|
|||||||
if(state.isPlaying()){
|
if(state.isPlaying()){
|
||||||
queue.run();
|
queue.run();
|
||||||
|
|
||||||
|
//TODO: WHICH clusters need to update here? do I iterate through 256 teams every time? ugh
|
||||||
clustersToUpdate.each(cluster -> {
|
clustersToUpdate.each(cluster -> {
|
||||||
|
|
||||||
|
|
||||||
//just in case: don't redundantly update inner clusters after you've recalculated it entirely
|
//just in case: don't redundantly update inner clusters after you've recalculated it entirely
|
||||||
clustersToInnerUpdate.remove(cluster);
|
clustersToInnerUpdate.remove(cluster);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user