diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index 9c9a4e812b..cc0d09d66e 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -3,10 +3,10 @@ package mindustry.ai; import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; -import arc.util.async.*; import mindustry.core.*; import mindustry.game.EventType.*; import mindustry.gen.*; @@ -17,9 +17,9 @@ import static mindustry.Vars.*; import static mindustry.ai.Pathfinder.*; //TODO I'm sure this class has countless problems -public class ControlPathfinder implements Runnable{ +public class ControlPathfinder{ private static final long maxUpdate = Time.millisToNanos(20); - private static final int updateFPS = 60; + private static final int updateFPS = 50; private static final int updateInterval = 1000 / updateFPS; public static boolean showDebug = false; @@ -48,19 +48,15 @@ public class ControlPathfinder implements Runnable{ //increments each tile change static volatile int worldUpdateId; - /** Current pathfinding thread */ - @Nullable Thread thread; + /** Current pathfinding threads, contents may be null */ + @Nullable PathfindThread[] threads; /** for unique target IDs */ int lastTargetId = 1; - /** handles task scheduling on the update thread. */ - TaskQueue queue = new TaskQueue(); /** requests per-unit */ ObjectMap requests = new ObjectMap<>(); - /** pathfinding thread access only! */ - Seq threadRequests = new Seq<>(); - public ControlPathfinder(){ + Events.on(WorldLoadEvent.class, event -> { stop(); wwidth = world.width(); @@ -81,7 +77,7 @@ public class ControlPathfinder implements Runnable{ //skipped N update -> drop it if(req.lastUpdateId <= state.updateId - 10){ requests.remove(req.unit); - queue.post(() -> threadRequests.remove(req)); + req.thread.queue.post(() -> req.thread.requests.remove(req)); } } }); @@ -91,10 +87,16 @@ public class ControlPathfinder implements Runnable{ for(var req : requests.values()){ if(req.frontier == null) continue; - //TODO will this even work Draw.draw(Layer.overlayUI, () -> { if(req.done){ int len = req.result.size; + int rp = req.rayPathIndex; + if(rp < len && rp >= 0){ + Draw.color(Color.royal); + Tile tile = tile(req.result.items[rp]); + Lines.line(req.unit.x, req.unit.y, tile.worldx(), tile.worldy()); + } + for(int i = 0; i < len; i++){ Draw.color(Tmp.c1.set(Color.white).fromHsv(i / (float)len * 360f, 1f, 0.9f)); int pos = req.result.items[i]; @@ -129,6 +131,8 @@ public class ControlPathfinder implements Runnable{ /** @return whether a path is ready */ public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){ + //uninitialized + if(threads == null) return false; int pathType = unit.pathType(); @@ -145,7 +149,9 @@ public class ControlPathfinder implements Runnable{ //check for request existence if(!requests.containsKey(unit)){ - var req = new PathRequest(); + PathfindThread thread = Structs.findMin(threads, t -> t.requestSize); + + var req = new PathRequest(thread); req.unit = unit; req.pathType = pathType; req.destination.set(destination); @@ -153,11 +159,13 @@ public class ControlPathfinder implements Runnable{ req.lastUpdateId = state.updateId; req.lastPos.set(unit); req.lastWorldUpdate = worldUpdateId; + //raycast immediately when done + req.raycastTimer = 9999f; requests.put(unit, req); //add to thread so it gets processed next update - queue.post(() -> threadRequests.add(req)); + thread.queue.post(() -> thread.requests.add(req)); }else{ var req = requests.get(unit); req.lastUpdateId = state.updateId; @@ -237,15 +245,24 @@ public class ControlPathfinder implements Runnable{ /** Starts or restarts the pathfinding thread. */ private void start(){ stop(); - thread = Threads.daemon("ControlPathfinder", this); + + //TODO currently capped at 6 threads, might be a good idea to make it more? + threads = new PathfindThread[Mathf.clamp(Runtime.getRuntime().availableProcessors() - 2, 1, 6)]; + for(int i = 0; i < threads.length; i ++){ + threads[i] = new PathfindThread("ControlPathfindThread-" + i); + threads[i].setDaemon(true); + threads[i].start(); + } } /** Stops the pathfinding thread. */ private void stop(){ - if(thread != null){ - thread.interrupt(); - thread = null; + if(threads != null){ + for(var thread : threads){ + thread.interrupt(); + } } + threads = null; requests.clear(); } @@ -341,39 +358,58 @@ public class ControlPathfinder implements Runnable{ return cost(type, b); } - @Override - public void run(){ - while(true){ - //stop on client, no updating - if(net.client()) return; - try{ - if(state.isPlaying()){ - queue.run(); + static class PathfindThread extends Thread{ + /** handles task scheduling on the update thread. */ + TaskQueue queue = new TaskQueue(); + /** pathfinding thread access only! */ + Seq requests = new Seq<>(); + /** volatile for access across threads */ + volatile int requestSize; - //total update time no longer than maxUpdate - for(var req : threadRequests){ - //TODO this is flawed with many paths - req.update(maxUpdate / requests.size); - } - } + public PathfindThread(String name){ + super(name); + } + @Override + public void run(){ + while(true){ + //stop on client, no updating + if(net.client()) return; try{ - Thread.sleep(updateInterval); - }catch(InterruptedException e){ - //stop looping when interrupted externally - return; + if(state.isPlaying()){ + queue.run(); + requestSize = requests.size; + + //total update time no longer than maxUpdate + for(var req : requests){ + //TODO this is flawed with many paths + req.update(maxUpdate / requests.size); + } + } + + try{ + Thread.sleep(updateInterval); + }catch(InterruptedException e){ + //stop looping when interrupted externally + return; + } + }catch(Throwable e){ + //do not crash the pathfinding thread + Log.err(e); } - }catch(Throwable e){ - //do not crash the pathfinding thread - Log.err(e); } } } //TODO each one of these could run in its own thread. static class PathRequest{ + final PathfindThread thread; + volatile boolean done = false; volatile boolean foundEnd = false; + volatile Unit unit; + volatile int pathType; + volatile int lastWorldUpdate; final Vec2 lastPos = new Vec2(); float stuckTimer = 0f; @@ -381,10 +417,6 @@ public class ControlPathfinder implements Runnable{ final Vec2 destination = new Vec2(); final Vec2 lastDestination = new Vec2(); - volatile Unit unit; - volatile int pathType; - volatile int lastWorldUpdate; - //TODO only access on main thread?? volatile int pathIndex; @@ -405,6 +437,10 @@ public class ControlPathfinder implements Runnable{ volatile int lastId, curId; + public PathRequest(PathfindThread thread){ + this.thread = thread; + } + void update(long maxUpdateNs){ if(curId != lastId){ clear(); @@ -508,7 +544,6 @@ public class ControlPathfinder implements Runnable{ void clear(){ done = false; - //TODO could be less expensive? frontier = new PathfindQueue(20); cameFrom.clear(); costs.clear(); diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index d97cb16b4f..93dfbe413f 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -3801,7 +3801,7 @@ public class Blocks{ worldProcessor = new LogicBlock("world-processor"){{ //currently incomplete, debugOnly for now - requirements(Category.logic, BuildVisibility.debugOnly, with()); + requirements(Category.logic, BuildVisibility.editorOnly, with()); //TODO customizable IPT instructionsPerTick = 8; diff --git a/core/src/mindustry/world/blocks/storage/CoreBlock.java b/core/src/mindustry/world/blocks/storage/CoreBlock.java index 63505ce503..6daf7c0a39 100644 --- a/core/src/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/mindustry/world/blocks/storage/CoreBlock.java @@ -122,6 +122,8 @@ public class CoreBlock extends StorageBlock{ @Override public boolean canPlaceOn(Tile tile, Team team, int rotation){ if(tile == null) return false; + //in the editor, you can place them anywhere for convenience + if(state.isEditor()) return true; CoreBuild core = team.core(); //must have all requirements if(core == null || (!state.rules.infiniteResources && !core.items.has(requirements, state.rules.buildCostMultiplier))) return false;