From b0cf1f7ef3728d9b57c60536297bd686bddb2f09 Mon Sep 17 00:00:00 2001 From: Anuken Date: Tue, 29 Apr 2025 12:23:09 -0400 Subject: [PATCH] Potential pathfinder spurious error fix --- core/src/mindustry/ai/ControlPathfinder.java | 94 +++++++++----------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index 195391083a..e59aa6707b 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -107,42 +107,44 @@ public class ControlPathfinder implements Runnable{ //maps team -> pathCost -> flattened array of clusters in 2D //(what about teams? different path costs?) - Cluster[][][] clusters; - - int cwidth, cheight; + final Cluster[][][] clusters = new Cluster[256][][]; + final int cwidth = Mathf.ceil((float)world.width() / clusterSize), cheight = Mathf.ceil((float)world.height() / clusterSize); //temporarily used for resolving connections for intra-edges - IntSet usedEdges = new IntSet(); + final IntSet usedEdges = new IntSet(); //tasks to run on pathfinding thread - TaskQueue queue = new TaskQueue(); + final TaskQueue queue = new TaskQueue(); //individual requests based on unit - MAIN THREAD ONLY - ObjectMap unitRequests = new ObjectMap<>(); + final ObjectMap unitRequests = new ObjectMap<>(); - Seq threadPathRequests = new Seq<>(false); + final Seq threadPathRequests = new Seq<>(false); //TODO: very dangerous usage; //TODO - it is accessed from the main thread //TODO - it is written to on the pathfinding thread //maps position in world in (x + y * width format) | path type | team (bitpacked to long with FieldIndex.get) to a cache of flow fields - LongMap fields = new LongMap<>(); + final LongMap fields = new LongMap<>(); //MAIN THREAD ONLY - Seq fieldList = new Seq<>(false); + final Seq fieldList = new Seq<>(false); //these are for inner edge A* (temporary!) - IntFloatMap innerCosts = new IntFloatMap(); - PathfindQueue innerFrontier = new PathfindQueue(); + final IntFloatMap innerCosts = new IntFloatMap(); + final PathfindQueue innerFrontier = new PathfindQueue(); //ONLY modify on pathfinding thread. - IntSet clustersToUpdate = new IntSet(); - IntSet clustersToInnerUpdate = new IntSet(); + final IntSet clustersToUpdate = new IntSet(); + final IntSet clustersToInnerUpdate = new IntSet(); //PATHFINDING THREAD - requests that should be recomputed - ObjectSet invalidRequests = new ObjectSet<>(); + final ObjectSet invalidRequests = new ObjectSet<>(); /** Current pathfinding thread */ @Nullable Thread thread; + /** If true, this pathfinder is no longer relevant (stopped) and its errors can be ignored. */ + volatile boolean invalidated; + //path requests are per-unit static class PathRequest{ final Unit unit; @@ -211,51 +213,38 @@ public class ControlPathfinder implements Runnable{ LongSeq[][] portalConnections = new LongSeq[4][]; } - public ControlPathfinder(){ - - Events.on(ResetEvent.class, event -> stop()); + static{ + Events.on(ResetEvent.class, event -> controlPath.stop()); Events.on(WorldLoadEvent.class, event -> { - stop(); - - //TODO: can the pathfinding thread even see these? - unitRequests = new ObjectMap<>(); - fields = new LongMap<>(); - fieldList = new Seq<>(false); - - clusters = new Cluster[256][][]; - cwidth = Mathf.ceil((float)world.width() / clusterSize); - cheight = Mathf.ceil((float)world.height() / clusterSize); - - - start(); + controlPath.stop(); + //create a new pathfinder to avoid contaminating the new pathfinding state with the old thread, which may still be running + controlPath = new ControlPathfinder(); + controlPath.start(); }); Events.on(TileChangeEvent.class, e -> { - - updateTile(e.tile); - - //TODO: recalculate affected flow fields? or just all of them? how to reflow? + controlPath.updateTile(e.tile); }); //invalidate paths Events.run(Trigger.update, () -> { - for(var req : unitRequests.values()){ + for(var req : controlPath.unitRequests.values()){ //skipped N update -> drop it if(req.lastUpdateId <= state.updateId - 10 || !req.unit.isAdded()){ req.invalidated = true; //concurrent modification! - queue.post(() -> threadPathRequests.remove(req)); - Core.app.post(() -> unitRequests.remove(req.unit)); + controlPath.queue.post(() -> controlPath.threadPathRequests.remove(req)); + Time.run(0f, () -> controlPath.unitRequests.remove(req.unit)); } } - for(var field : fieldList){ + for(var field : controlPath.fieldList){ //skipped N update -> drop it if(field.lastUpdateId <= state.updateId - 30){ //make sure it's only modified on the main thread...? but what about calling get() on this thread?? - queue.post(() -> fields.remove(field.mapKey)); - Core.app.post(() -> fieldList.remove(field)); + controlPath.queue.post(() -> controlPath.fields.remove(field.mapKey)); + Time.run(0f, () -> controlPath.fieldList.remove(field)); } } }); @@ -268,11 +257,11 @@ public class ControlPathfinder implements Runnable{ Draw.draw(Layer.overlayUI, () -> { Lines.stroke(1f); - if(clusters[team] != null && clusters[team][cost] != null){ - for(int cx = 0; cx < cwidth; cx++){ - for(int cy = 0; cy < cheight; cy++){ + if(controlPath.clusters[team] != null && controlPath.clusters[team][cost] != null){ + for(int cx = 0; cx < controlPath.cwidth; cx++){ + for(int cy = 0; cy < controlPath.cheight; cy++){ - var cluster = clusters[team][cost][cy * cwidth + cx]; + var cluster = controlPath.clusters[team][cost][cy * controlPath.cwidth + cx]; if(cluster != null){ Lines.stroke(0.5f); Draw.color(Color.gray); @@ -290,7 +279,7 @@ public class ControlPathfinder implements Runnable{ int from = Point2.x(pos), to = Point2.y(pos); float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; - portalToVec(cluster, cx, cy, d, i, Tmp.v1); + controlPath.portalToVec(cluster, cx, cy, d, i, Tmp.v1); Draw.color(Color.brown); Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); @@ -302,7 +291,7 @@ public class ControlPathfinder implements Runnable{ for(int coni = 0; coni < connections.size; coni ++){ long con = connections.items[coni]; - portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); + controlPath.portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); float x1 = Tmp.v1.x, y1 = Tmp.v1.y, @@ -319,10 +308,10 @@ public class ControlPathfinder implements Runnable{ } } - for(var fields : fieldList){ + for(var fields : controlPath.fieldList){ try{ for(var entry : fields.fields){ - int cx = entry.key % cwidth, cy = entry.key / cwidth; + int cx = entry.key % controlPath.cwidth, cy = entry.key / controlPath.cwidth; for(int y = 0; y < clusterSize; y++){ for(int x = 0; x < clusterSize; x++){ int value = entry.value[x + y * clusterSize]; @@ -402,6 +391,7 @@ public class ControlPathfinder implements Runnable{ thread.interrupt(); thread = null; } + invalidated = true; queue.clear(); } @@ -1502,8 +1492,6 @@ public class ControlPathfinder implements Runnable{ while(true){ if(net.client()) return; try{ - - if(state.isPlaying()){ queue.run(); @@ -1586,7 +1574,11 @@ public class ControlPathfinder implements Runnable{ return; } }catch(Throwable e){ - Log.err(e); + if(!invalidated){ + Log.err(e); + }else{ + return; + } } } }