From ca30f49481f697a15c907ad5b1c5fe2f69212e5c Mon Sep 17 00:00:00 2001
From: Anuken <arnukren@gmail.com>
Date: Wed, 20 Jun 2018 22:56:53 -0400
Subject: [PATCH] Added spatial indexing of tile targets, many bugfixes

---
 .../io/anuke/mindustry/ai/BlockIndexer.java   | 59 ++++++++++++++++++-
 .../io/anuke/mindustry/ai/WaveSpawner.java    |  2 +-
 core/src/io/anuke/mindustry/core/World.java   |  4 +-
 .../io/anuke/mindustry/entities/Player.java   |  2 +-
 .../io/anuke/mindustry/entities/Units.java    | 45 ++++----------
 .../mindustry/entities/units/BaseUnit.java    |  5 +-
 .../mindustry/entities/units/GroundUnit.java  |  3 +-
 .../src/io/anuke/mindustry/game/TeamInfo.java | 12 ++++
 core/src/io/anuke/mindustry/io/Map.java       |  5 ++
 .../anuke/mindustry/io/versions/Save16.java   | 16 +++--
 .../anuke/mindustry/world/mapgen/ProcGen.java |  4 +-
 11 files changed, 105 insertions(+), 52 deletions(-)

diff --git a/core/src/io/anuke/mindustry/ai/BlockIndexer.java b/core/src/io/anuke/mindustry/ai/BlockIndexer.java
index aa911488ca..76b4a3ac46 100644
--- a/core/src/io/anuke/mindustry/ai/BlockIndexer.java
+++ b/core/src/io/anuke/mindustry/ai/BlockIndexer.java
@@ -1,10 +1,12 @@
 package io.anuke.mindustry.ai;
 
+import com.badlogic.gdx.math.Vector2;
 import com.badlogic.gdx.utils.Bits;
 import com.badlogic.gdx.utils.IntMap;
 import com.badlogic.gdx.utils.ObjectMap;
 import com.badlogic.gdx.utils.ObjectSet;
 import io.anuke.mindustry.content.Items;
+import io.anuke.mindustry.entities.TileEntity;
 import io.anuke.mindustry.game.EventType.TileChangeEvent;
 import io.anuke.mindustry.game.EventType.WorldLoadEvent;
 import io.anuke.mindustry.game.Team;
@@ -13,11 +15,12 @@ import io.anuke.mindustry.type.Item;
 import io.anuke.mindustry.world.Tile;
 import io.anuke.mindustry.world.meta.BlockFlag;
 import io.anuke.ucore.core.Events;
+import io.anuke.ucore.entities.trait.Entity;
+import io.anuke.ucore.function.Predicate;
 import io.anuke.ucore.util.EnumSet;
 import io.anuke.ucore.util.Mathf;
 
-import static io.anuke.mindustry.Vars.state;
-import static io.anuke.mindustry.Vars.world;
+import static io.anuke.mindustry.Vars.*;
 
 //TODO consider using quadtrees for finding specific types of blocks within an area
 //TODO maybe use Arrays instead of ObjectSets?
@@ -77,6 +80,12 @@ public class BlockIndexer {
                 }
             }
 
+            for (int x = 0; x < quadWidth(); x++) {
+                for (int y = 0; y < quadHeight(); y++) {
+                    updateQuadrant(world.tile(x * structQuadrantSize, y * structQuadrantSize));
+                }
+            }
+
             scanOres();
         });
     }
@@ -91,6 +100,36 @@ public class BlockIndexer {
         return (!state.teams.get(team).ally ? allyMap : enemyMap).get(type, emptyArray);
     }
 
+    public TileEntity findTile(Team team, float x, float y, float range, Predicate<Tile> pred){
+        Entity closest = null;
+        float dst = 0;
+
+        for(int rx = Math.max((int)((x-range)/tilesize/structQuadrantSize), 0); rx <= (int)((x+range)/tilesize/structQuadrantSize) && rx < quadWidth(); rx ++){
+            for(int ry = Math.max((int)((y-range)/tilesize/structQuadrantSize), 0); ry <= (int)((y+range)/tilesize/structQuadrantSize) && ry < quadHeight(); ry ++){
+
+                if(!getQuad(team, rx, ry)) continue;
+
+                for(int tx = rx * structQuadrantSize; tx < (rx + 1) * structQuadrantSize && tx < world.width(); tx ++){
+                    for(int ty = ry * structQuadrantSize; ty < (ry + 1) * structQuadrantSize && ty < world.height(); ty ++ ){
+                        Tile other = world.tile(tx, ty);
+
+                        if(other == null || other.entity == null || !pred.test(other)) continue;
+
+                        TileEntity e = other.entity;
+
+                        float ndst = Vector2.dst(x, y, e.x, e.y);
+                        if(ndst < range && (closest == null || ndst < dst)){
+                            dst = ndst;
+                            closest = e;
+                        }
+                    }
+                }
+            }
+        }
+
+        return (TileEntity) closest;
+    }
+
     /**Returns a set of tiles that have ores of the specified type nearby.
      * While each tile in the set is not guaranteed to have an ore directly on it,
      * each tile will at least have an ore within {@link #oreQuadrantSize} / 2 blocks of it.
@@ -124,7 +163,8 @@ public class BlockIndexer {
         //this quadrant is now 'dirty', re-scan the whole thing
         int quadrantX = tile.x / structQuadrantSize;
         int quadrantY = tile.y / structQuadrantSize;
-        int index = quadrantX * Mathf.ceil(world.width() / (float)structQuadrantSize) + quadrantY;
+        int index = quadrantX + quadrantY * quadWidth();
+        //Log.info("Updating quadrant: {0} {1}", quadrantX, quadrantY);
 
         for(TeamData data : state.teams.getTeams()) {
 
@@ -150,6 +190,19 @@ public class BlockIndexer {
         }
     }
 
+    private boolean getQuad(Team team, int quadrantX, int quadrantY){
+        int index = quadrantX + quadrantY * Mathf.ceil(world.width() / (float)structQuadrantSize);
+        return structQuadrants[team.ordinal()].get(index);
+    }
+
+    private int quadWidth(){
+        return Mathf.ceil(world.width() / (float)structQuadrantSize);
+    }
+
+    private int quadHeight(){
+        return Mathf.ceil(world.height() / (float)structQuadrantSize);
+    }
+
     private ObjectMap<BlockFlag, ObjectSet<Tile>> getMap(Team team){
         if(!state.teams.has(team)) return emptyMap;
         return state.teams.get(team).ally ? allyMap : enemyMap;
diff --git a/core/src/io/anuke/mindustry/ai/WaveSpawner.java b/core/src/io/anuke/mindustry/ai/WaveSpawner.java
index e8e39aad34..7252dba755 100644
--- a/core/src/io/anuke/mindustry/ai/WaveSpawner.java
+++ b/core/src/io/anuke/mindustry/ai/WaveSpawner.java
@@ -105,7 +105,7 @@ public class WaveSpawner {
             for (int y = quady * quadsize; y < world.height() && y < (quady + 1)*quadsize; y++) {
                 Tile tile = world.tile(x, y);
 
-                if(tile.solid() || world.pathfinder().getValueforTeam(Team.red, x, y) == Float.MAX_VALUE){
+                if(tile == null || tile.solid() || world.pathfinder().getValueforTeam(Team.red, x, y) == Float.MAX_VALUE){
                     setQuad(quadx, quady, false);
                     break outer;
                 }
diff --git a/core/src/io/anuke/mindustry/core/World.java b/core/src/io/anuke/mindustry/core/World.java
index 7a893461be..4680cddbd0 100644
--- a/core/src/io/anuke/mindustry/core/World.java
+++ b/core/src/io/anuke/mindustry/core/World.java
@@ -93,11 +93,11 @@ public class World extends Module{
 	}
 	
 	public int width(){
-		return currentMap.meta.width;
+		return tiles.length;
 	}
 	
 	public int height(){
-		return currentMap.meta.height;
+		return tiles[0].length;
 	}
 
 	public Tile tile(int packed){
diff --git a/core/src/io/anuke/mindustry/entities/Player.java b/core/src/io/anuke/mindustry/entities/Player.java
index 52bbf7d47d..a85f9daaff 100644
--- a/core/src/io/anuke/mindustry/entities/Player.java
+++ b/core/src/io/anuke/mindustry/entities/Player.java
@@ -662,7 +662,7 @@ public class Player extends Unit implements BuilderTrait, CarryTrait {
 		if(local){
 			int index = stream.readByte();
 			players[index].readSaveSuper(stream);
-			dead = false;
+			players[index].dead = false;
 		}
 	}
 
diff --git a/core/src/io/anuke/mindustry/entities/Units.java b/core/src/io/anuke/mindustry/entities/Units.java
index d672cab2ff..ad4a81fdc0 100644
--- a/core/src/io/anuke/mindustry/entities/Units.java
+++ b/core/src/io/anuke/mindustry/entities/Units.java
@@ -10,10 +10,8 @@ import io.anuke.mindustry.world.Block;
 import io.anuke.mindustry.world.Tile;
 import io.anuke.ucore.entities.EntityGroup;
 import io.anuke.ucore.entities.EntityPhysics;
-import io.anuke.ucore.entities.trait.Entity;
 import io.anuke.ucore.function.Consumer;
 import io.anuke.ucore.function.Predicate;
-import io.anuke.ucore.util.Mathf;
 
 import static io.anuke.mindustry.Vars.*;
 
@@ -91,43 +89,24 @@ public class Units {
 
     /**Returns the neareset ally tile in a range.*/
     public static TileEntity findAllyTile(Team team, float x, float y, float range, Predicate<Tile> pred){
-        return findTile(x, y, range, tile -> !state.teams.areEnemies(team, tile.getTeam()) && pred.test(tile));
+        for(Team enemy : state.teams.alliesOf(team)){
+            TileEntity entity = world.indexer().findTile(enemy, x, y, range, pred);
+            if(entity != null){
+                return entity;
+            }
+        }
+        return null;
     }
 
     /**Returns the neareset enemy tile in a range.*/
     public static TileEntity findEnemyTile(Team team, float x, float y, float range, Predicate<Tile> pred){
-        return findTile(x, y, range, tile -> state.teams.areEnemies(team, tile.getTeam()) && pred.test(tile));
-    }
-
-    //TODO optimize, spatial caching of tiles
-    /**Returns the neareset tile entity in a range.*/
-    public static TileEntity findTile(float x, float y, float range, Predicate<Tile> pred){
-        Entity closest = null;
-        float dst = 0;
-
-        int rad = (int)(range/tilesize)+1;
-        int tilex = Mathf.scl2(x, tilesize);
-        int tiley = Mathf.scl2(y, tilesize);
-
-        for(int rx = -rad; rx <= rad; rx ++){
-            for(int ry = -rad; ry <= rad; ry ++){
-                Tile other = world.tile(rx+tilex, ry+tiley);
-
-                if(other != null) other = other.target();
-
-                if(other == null || other.entity == null || !pred.test(other)) continue;
-
-                TileEntity e = other.entity;
-
-                float ndst = Vector2.dst(x, y, e.x, e.y);
-                if(ndst < range && (closest == null || ndst < dst)){
-                    dst = ndst;
-                    closest = e;
-                }
+        for(Team enemy : state.teams.enemiesOf(team)){
+            TileEntity entity = world.indexer().findTile(enemy, x, y, range, pred);
+            if(entity != null){
+                return entity;
             }
         }
-
-        return (TileEntity) closest;
+        return null;
     }
 
     /**Iterates over all units on all teams, including players.*/
diff --git a/core/src/io/anuke/mindustry/entities/units/BaseUnit.java b/core/src/io/anuke/mindustry/entities/units/BaseUnit.java
index 2243e14f6e..01e684164a 100644
--- a/core/src/io/anuke/mindustry/entities/units/BaseUnit.java
+++ b/core/src/io/anuke/mindustry/entities/units/BaseUnit.java
@@ -5,6 +5,7 @@ import io.anuke.annotations.Annotations.Remote;
 import io.anuke.mindustry.content.fx.ExplosionFx;
 import io.anuke.mindustry.entities.TileEntity;
 import io.anuke.mindustry.entities.Unit;
+import io.anuke.mindustry.entities.Units;
 import io.anuke.mindustry.entities.bullet.Bullet;
 import io.anuke.mindustry.entities.traits.TargetTrait;
 import io.anuke.mindustry.game.Team;
@@ -113,7 +114,7 @@ public abstract class BaseUnit extends Unit{
 	public void targetClosest(){
 		if(target != null) return;
 
-		//target = Units.getClosestTarget(team, x, y, inventory.getAmmoRange());
+		target = Units.getClosestTarget(team, x, y, inventory.getAmmoRange());
 	}
 
 	public UnitState getStartState(){
@@ -254,12 +255,14 @@ public abstract class BaseUnit extends Unit{
 	public void writeSave(DataOutput stream) throws IOException {
 		super.writeSave(stream);
 		stream.writeByte(type.id);
+		stream.writeBoolean(isWave);
 	}
 
 	@Override
 	public void readSave(DataInput stream) throws IOException {
 		super.readSave(stream);
 		byte type = stream.readByte();
+		this.isWave = stream.readBoolean();
 
 		this.type = UnitType.getByID(type);
 		add();
diff --git a/core/src/io/anuke/mindustry/entities/units/GroundUnit.java b/core/src/io/anuke/mindustry/entities/units/GroundUnit.java
index df577c613d..5102941d29 100644
--- a/core/src/io/anuke/mindustry/entities/units/GroundUnit.java
+++ b/core/src/io/anuke/mindustry/entities/units/GroundUnit.java
@@ -53,7 +53,7 @@ public abstract class GroundUnit extends BaseUnit {
     public void update() {
         super.update();
 
-        if(target == null){
+        if(!velocity.isZero(0.001f) && (target == null || (inventory.hasAmmo() && distanceTo(target) <= inventory.getAmmoRange()))){
             rotation = Mathf.lerpDelta(rotation, velocity.angle(), 0.2f);
         }
     }
@@ -104,7 +104,6 @@ public abstract class GroundUnit extends BaseUnit {
         super.updateTargeting();
 
         if(Units.invalidateTarget(target, team,  x, y, Float.MAX_VALUE)){
-            if(target != null) Log.info("Invalidating target {0}", target);
             target = null;
         }
     }
diff --git a/core/src/io/anuke/mindustry/game/TeamInfo.java b/core/src/io/anuke/mindustry/game/TeamInfo.java
index b55fe91308..33ea3b37c7 100644
--- a/core/src/io/anuke/mindustry/game/TeamInfo.java
+++ b/core/src/io/anuke/mindustry/game/TeamInfo.java
@@ -76,6 +76,18 @@ public class TeamInfo {
         return ally ? enemies : allies;
     }
 
+    /**Returns a set of all teams that are allies of this team.
+     * For teams not active, an empty set is returned.*/
+    public ObjectSet<Team> alliesOf(Team team) {
+        boolean ally = allies.contains(team);
+        boolean enemy = enemies.contains(team);
+
+        //this team isn't even in the game, so target everything!
+        if(!ally && !enemy) return allTeams;
+
+        return !ally ? enemies : allies;
+    }
+
     /**Returns a set of all teams that are enemies of this team.
      * For teams not active, an empty set is returned.*/
     public ObjectSet<TeamData> enemyDataOf(Team team) {
diff --git a/core/src/io/anuke/mindustry/io/Map.java b/core/src/io/anuke/mindustry/io/Map.java
index 6f4c36c929..1963995a4c 100644
--- a/core/src/io/anuke/mindustry/io/Map.java
+++ b/core/src/io/anuke/mindustry/io/Map.java
@@ -1,6 +1,7 @@
 package io.anuke.mindustry.io;
 
 import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.utils.ObjectMap;
 import io.anuke.ucore.function.Supplier;
 
 import java.io.InputStream;
@@ -24,6 +25,10 @@ public class Map {
         this.stream = streamSupplier;
     }
 
+    public Map(String unknownName, int width, int height){
+        this(unknownName, new MapMeta(0, new ObjectMap<>(), width, height, null), true, () -> null);
+    }
+
     public String getDisplayName(){
         return meta.tags.get("name", name);
     }
diff --git a/core/src/io/anuke/mindustry/io/versions/Save16.java b/core/src/io/anuke/mindustry/io/versions/Save16.java
index 4703700755..c241205f1c 100644
--- a/core/src/io/anuke/mindustry/io/versions/Save16.java
+++ b/core/src/io/anuke/mindustry/io/versions/Save16.java
@@ -9,13 +9,13 @@ import io.anuke.mindustry.entities.traits.TypeTrait;
 import io.anuke.mindustry.game.Difficulty;
 import io.anuke.mindustry.game.GameMode;
 import io.anuke.mindustry.game.Team;
+import io.anuke.mindustry.io.Map;
 import io.anuke.mindustry.io.SaveFileVersion;
 import io.anuke.mindustry.world.Block;
 import io.anuke.mindustry.world.Tile;
 import io.anuke.mindustry.world.blocks.BlockPart;
 import io.anuke.ucore.entities.Entities;
 import io.anuke.ucore.entities.EntityGroup;
-import io.anuke.ucore.entities.EntityPhysics;
 import io.anuke.ucore.entities.trait.Entity;
 import io.anuke.ucore.util.Bits;
 
@@ -23,7 +23,8 @@ import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
 
-import static io.anuke.mindustry.Vars.*;
+import static io.anuke.mindustry.Vars.state;
+import static io.anuke.mindustry.Vars.world;
 
 public class Save16 extends SaveFileVersion {
 
@@ -39,7 +40,8 @@ public class Save16 extends SaveFileVersion {
         //general state
         byte mode = stream.readByte();
         String mapname = stream.readUTF();
-        world.setMap(world.maps().getByName(mapname));
+        Map map = world.maps().getByName(mapname);
+        world.setMap(map);
 
         int wave = stream.readInt();
         byte difficulty = stream.readByte();
@@ -55,13 +57,13 @@ public class Save16 extends SaveFileVersion {
 
         int blocksize = stream.readInt();
 
-        IntMap<Block> map = new IntMap<>();
+        IntMap<Block> blockMap = new IntMap<>();
 
         for(int i = 0; i < blocksize; i ++){
             String name = stream.readUTF();
             int id = stream.readShort();
 
-            map.put(id, Block.getByName(name));
+            blockMap.put(id, Block.getByName(name));
         }
 
         //entities
@@ -82,7 +84,9 @@ public class Save16 extends SaveFileVersion {
         short width = stream.readShort();
         short height = stream.readShort();
 
-        EntityPhysics.resizeTree(0, 0, width * tilesize, height * tilesize);
+        if(map == null){
+            world.setMap(new Map("unknown", width, height));
+        }
 
         world.beginMapLoad();
 
diff --git a/core/src/io/anuke/mindustry/world/mapgen/ProcGen.java b/core/src/io/anuke/mindustry/world/mapgen/ProcGen.java
index 2245f40232..5826fee28f 100644
--- a/core/src/io/anuke/mindustry/world/mapgen/ProcGen.java
+++ b/core/src/io/anuke/mindustry/world/mapgen/ProcGen.java
@@ -41,9 +41,7 @@ public class ProcGen {
 
                 if(dst < 20){
                     elevation = 0;
-                }
-
-                if(r > 0.9){
+                }else if(r > 0.9){
                     marker.floor = (byte)Blocks.water.id;
                     elevation = 0;