/*
* Aphelion
* Copyright (c) 2014 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.shared.swissarmyknife;
import aphelion.shared.physics.valueobjects.PhysicsPoint;
import static aphelion.shared.swissarmyknife.SwissArmyKnife.clip;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Fast way to look up entities by their position.
* (At the expensive of memory).
* @author Joris
* @param <T>
*/
public class EntityGrid<T extends EntityGridEntity>
{
private final LinkedListHead<T>[][] grid;
/** The size of a single cell in the entity grid.
* Must be a factor of 2!
*/
public final int CELL_SIZE;
/** The size of the entire grid (per axis).
*
*/
public final int GRID_SIZE;
/**
* Are actions being queued? (until disableQueue() is called)
*/
private boolean queue_enabled = false;
/**
* The queue containing actions to execute upon disableQueue()
* Grows as needed and stays that way to prevent allocation memory over and over
*/
private final ArrayList<QueueEntry<T>> queue = new ArrayList<>();
/** The index of the last entry with actual data.
*/
private int queue_currentIndex = -1;
/**
* @param cellSize The size of a single cell in the entity grid.
* Must be a factor of 2!
* @param cells The size of the entire grid (per axis).
*/
public EntityGrid(int cellSize, int cells)
{
this.CELL_SIZE = cellSize;
this.GRID_SIZE = cells;
this.grid = new LinkedListHead[GRID_SIZE][GRID_SIZE];
for (int x = 0; x < GRID_SIZE; ++x)
{
for (int y = 0; y < GRID_SIZE; ++y)
{
grid[x][y] = new LinkedListHead<>();
}
}
}
private QueueEntry<T> getEmptyQueueEntry()
{
int index = ++queue_currentIndex;
QueueEntry<T> ret;
if (index >= queue.size())
{
ret = new QueueEntry<>();
queue.add(ret);
return ret;
}
return queue.get(index);
}
/** Remove an entity from the grid.
*
* @param entity
*/
public void removeEntity(@Nonnull T entity)
{
LinkedListEntry link;
QueueEntry<T> entry;
if (this.queue_enabled)
{
entry = this.getEmptyQueueEntry();
entry.removeAction = true;
entry.entity = entity;
return;
}
link = entity.getEntityGridEntry(this);
link.remove();
}
/** Update an entity to its new location on the grid.
* Or remove it from the grid entirely if the entity no longer has a valid location.
*
* @param entity
* @param x
* @param y
*/
public void updateLocation(@Nonnull T entity, int x, int y)
{
LinkedListEntry<T> link;
QueueEntry<T> entry;
if (this.queue_enabled)
{
entry = this.getEmptyQueueEntry();
entry.removeAction = false;
entry.x = x;
entry.y = y;
entry.entity = entity;
return;
}
link = entity.getEntityGridEntry(this);
link.remove();
// Because ENTITY_GRID_CELL_SIZE is a multiple of 2,
// This should optimize to a simple bitshift
// If support for negative values were to be added, floor() should probably be used.
// Currently negative values > -CELL_SIZE are stored in cell 0, this is okay.
int cell_x = x / CELL_SIZE;
int cell_y = y / CELL_SIZE;
try
{
LinkedListHead<T> cell = grid[cell_x][cell_y];
cell.append(link);
}
catch (IndexOutOfBoundsException ex)
{
// do nothing, the entity will not be part of the collision
// This means the projectile is outside of the map
}
}
/** Update an entity to its new location on the grid.
* Or remove it from the grid entirely if the entity no longer has a valid location.
*
* @param entity
* @param pos If set, calculate the new grid position (using a fast bit shift),
* if not set remove the entity from the grid (or if the entity is outside of the map borders).
* @return True if the entity is now present in the grid.
*/
public void updateLocation(@Nonnull T entity, @Nullable PhysicsPoint pos)
{
if (pos == null || !pos.set)
{
removeEntity(entity);
}
else
{
updateLocation(entity, pos.x, pos.y);
}
}
/** Iterate over all the entities in the given square. This iterator is not safe against modifications
* (however it will not crash)
* @param low
* @param high
* @return
*/
public Iterator<T> iterator(final PhysicsPoint low, final PhysicsPoint high)
{
return new Iterator<T>()
{
final PhysicsPoint start = new PhysicsPoint();
final PhysicsPoint end = new PhysicsPoint();
int x;
int y;
LinkedListEntry<T> next;
{
start.set = true;
start.x = clip(low.x / CELL_SIZE, 0, GRID_SIZE-1);
start.y = clip(low.y / CELL_SIZE, 0, GRID_SIZE-1);
end.set = true;
end.x = clip(high.x / CELL_SIZE, 0, GRID_SIZE-1);
end.y = clip(high.y / CELL_SIZE, 0, GRID_SIZE-1);
x = start.x;
y = start.y;
findNext();
}
void findNext()
{
if (next != null)
{
next = next.next;
}
while (next == null && y <= end.y)
{
next = grid[x][y].first;
++x;
if (x > end.x)
{
x = start.x;
++y;
}
}
}
@Override
public boolean hasNext()
{
return next != null;
}
@Override
public T next()
{
if (!hasNext())
{
throw new NoSuchElementException();
}
try
{
return next.data;
}
finally
{
findNext();
}
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
/** Iterate over all the entities within the given radius (square). This iterator is not safe against modifications
* (however it will not crash) unless you call enableQueue() first.
* @param center
* @param radius
* @return
*/
public Iterator<T> iterator(final PhysicsPoint center, int radius)
{
final PhysicsPoint low = new PhysicsPoint(center);
final PhysicsPoint high = new PhysicsPoint(center);
low.sub(radius);
high.add(radius);
return iterator(low, high);
}
/**
* Enable queuing of any change to the grid. Enable this during iteration to prevent modifications messing up
* this iteration.
* Note: if multiple iterators of the same entity grid are needed at the same time (with different views), a
* different implementation will be needed
* @throws IllegalStateException The queue is already enabled
*/
public void enableQueue()
{
if (this.queue_enabled)
{
throw new IllegalStateException("EntityGrid queue is already enabled");
}
this.queue_enabled = true;
}
/**
* Disable the queue enabled by enableQueue() and apply the changes in the queue.
* @throws IllegalStateException The queue is already disabled
*/
public void disableQueue()
{
if (!this.queue_enabled)
{
throw new IllegalStateException("EntityGrid queue is already disabled");
}
this.queue_enabled = false;
for (int i = 0; i <= this.queue_currentIndex; ++i)
{
QueueEntry<T> entry = this.queue.get(i);
if (entry.removeAction)
{
this.removeEntity(entry.entity);
}
else
{
this.updateLocation(entry.entity, entry.x, entry.y);
}
entry.reset();
}
// clear the queue without removing the objects
this.queue_currentIndex = -1;
}
private static class QueueEntry<T>
{
boolean removeAction;
int x;
int y;
T entity;
public void reset()
{
removeAction = false;
x = 0;
y = 0;
entity = null;
}
}
}