diff --git a/core/assets/maps/atolls.msav b/core/assets/maps/atolls.msav index 02f2737deb..7fb5a58537 100644 Binary files a/core/assets/maps/atolls.msav and b/core/assets/maps/atolls.msav differ diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index e7bca60cd6..7531b849b3 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -170,6 +170,8 @@ public class Vars implements Loadable{ public static boolean confirmExit = true; /** if true, UI is not drawn */ public static boolean disableUI; + /** if true, most autosaving is disabled. internal use only! */ + public static boolean disableSave; /** if true, game is set up in mobile mode, even on desktop. used for debugging */ public static boolean testMobile; /** whether the game is running on a mobile device */ diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index f3ed9130d7..a4586e0873 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -124,7 +124,7 @@ public class ControlPathfinder implements Runnable{ //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) | type (bitpacked to long) to a cache of flow fields + //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<>(); //MAIN THREAD ONLY Seq fieldList = new Seq<>(false); @@ -188,6 +188,7 @@ public class ControlPathfinder implements Runnable{ final IntQueue frontier = new IntQueue(); //maps cluster index to field weights; 0 means uninitialized final IntMap fields = new IntMap<>(); + //packed (goalPos | costId | team) long key to use in the global fields map final long mapKey; //main thread only! @@ -200,7 +201,7 @@ public class ControlPathfinder implements Runnable{ this.team = team; this.goalPos = goalPos; this.costId = costId; - this.mapKey = Pack.longInt(goalPos, costId); + this.mapKey = FieldIndex.get(goalPos, costId, team); } } @@ -241,7 +242,7 @@ public class ControlPathfinder implements Runnable{ Events.run(Trigger.update, () -> { for(var req : unitRequests.values()){ //skipped N update -> drop it - if(req.lastUpdateId <= state.updateId - 10){ + if(req.lastUpdateId <= state.updateId - 10 || !req.unit.isAdded()){ req.invalidated = true; //concurrent modification! queue.post(() -> threadPathRequests.remove(req)); @@ -1024,10 +1025,12 @@ public class ControlPathfinder implements Runnable{ //no result found, bail out. if(nodePath == null){ request.notFound = true; + //stop following the old path, it's not relevant now, it's just not possible to reach the destination anymore + request.oldCache = null; return; } - FieldCache cache = fields.get(Pack.longInt(goalPos, costId)); + FieldCache cache = fields.get(FieldIndex.get(goalPos, costId, team)); //if true, extra values are added on the sides of existing field cells that face new cells. boolean addingFrontier = true; @@ -1143,7 +1146,7 @@ public class ControlPathfinder implements Runnable{ boolean any = false; - long fieldKey = Pack.longInt(destPos, costId); + long fieldKey = FieldIndex.get(destPos, costId, team); //use existing request if it exists. if(request != null && request.destination == destPos){ @@ -1152,13 +1155,14 @@ public class ControlPathfinder implements Runnable{ Tile tileOn = unit.tileOn(), initialTileOn = tileOn; //TODO: should fields be accessible from this thread? FieldCache fieldCache = fields.get(fieldKey); + if(fieldCache == null) fieldCache = request.oldCache; if(fieldCache != null && tileOn != null){ FieldCache old = request.oldCache; FieldCache targetCache = old != null ? old : fieldCache; boolean requeue = old == null; //nullify the old field to be GCed, as it cannot be relevant anymore (this path is complete) - if(fieldCache.frontier.isEmpty() && old != null){ + if(fieldCache != request.oldCache && fieldCache.frontier.isEmpty() && old != null){ request.oldCache = null; } @@ -1449,7 +1453,7 @@ public class ControlPathfinder implements Runnable{ int index = cx + cy * cwidth; for(var req : threadPathRequests){ - long mapKey = Pack.longInt(req.destination, pathCost); + long mapKey = FieldIndex.get(req.destination, pathCost, team); var field = fields.get(mapKey); if((field != null && field.fields.containsKey(index)) || req.notFound){ invalidRequests.add(req); @@ -1535,7 +1539,7 @@ public class ControlPathfinder implements Runnable{ continue; } - long mapKey = Pack.longInt(request.destination, request.costId); + long mapKey = FieldIndex.get(request.destination, request.costId, request.team); var field = fields.get(mapKey); @@ -1543,7 +1547,7 @@ public class ControlPathfinder implements Runnable{ //it's only worth recalculating a path when the current frontier has finished; otherwise the unit will be following something incomplete. if(field.frontier.isEmpty()){ - //remove the field, to be recalculated next update one recalculatePath is processed + //remove the field, to be recalculated next update once recalculatePath is processed fields.remove(field.mapKey); Core.app.post(() -> fieldList.remove(field)); @@ -1551,6 +1555,10 @@ public class ControlPathfinder implements Runnable{ for(var otherRequest : threadPathRequests){ if(otherRequest.destination == request.destination){ otherRequest.oldCache = field; + + if(otherRequest != request){ + queue.post(() -> recalculatePath(otherRequest)); + } } } @@ -1584,6 +1592,15 @@ public class ControlPathfinder implements Runnable{ } } + @Struct + static class FieldIndexStruct{ + int pos; + @StructField(8) + int costId; + @StructField(8) + int team; + } + @Struct static class IntraEdgeStruct{ @StructField(8) diff --git a/core/src/mindustry/game/Saves.java b/core/src/mindustry/game/Saves.java index 6927813008..575c8bb697 100644 --- a/core/src/mindustry/game/Saves.java +++ b/core/src/mindustry/game/Saves.java @@ -111,7 +111,7 @@ public class Saves{ if(state.isGame() && !state.gameOver && current != null && current.isAutosave()){ time += Time.delta; - if(time > Core.settings.getInt("saveinterval") * 60){ + if(time > Core.settings.getInt("saveinterval") * 60 && !Vars.disableSave){ saving = true; try{ diff --git a/core/src/mindustry/ui/dialogs/PausedDialog.java b/core/src/mindustry/ui/dialogs/PausedDialog.java index 677b3d53ed..2ff15e272b 100644 --- a/core/src/mindustry/ui/dialogs/PausedDialog.java +++ b/core/src/mindustry/ui/dialogs/PausedDialog.java @@ -159,7 +159,7 @@ public class PausedDialog extends BaseDialog{ return; } - if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || wasClient || state.gameOver){ + if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || wasClient || state.gameOver || disableSave){ logic.reset(); return; }