/*******************************************************************************
* Copyright (c) 2015, 2016
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*******************************************************************************/
package jsettlers.mapcreator.data;
import java.io.IOException;
import java.io.InputStream;
import java.util.BitSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import jsettlers.algorithms.partitions.IBlockingProvider;
import jsettlers.algorithms.partitions.PartitionCalculatorAlgorithm;
import jsettlers.algorithms.previewimage.IPreviewImageDataSupplier;
import jsettlers.common.CommonConstants;
import jsettlers.common.landscape.ELandscapeType;
import jsettlers.common.landscape.EResourceType;
import jsettlers.common.logging.MilliStopWatch;
import jsettlers.common.map.IGraphicsBackgroundListener;
import jsettlers.common.map.IMapData;
import jsettlers.common.map.object.BuildingObject;
import jsettlers.common.map.object.MapDecorationObject;
import jsettlers.common.map.object.MapObject;
import jsettlers.common.map.object.MapStoneObject;
import jsettlers.common.map.object.MapTreeObject;
import jsettlers.common.map.object.MovableObject;
import jsettlers.common.map.object.StackObject;
import jsettlers.common.map.shapes.IMapArea;
import jsettlers.common.movable.EDirection;
import jsettlers.common.movable.IMovable;
import jsettlers.common.position.RelativePoint;
import jsettlers.common.position.ShortPoint2D;
import jsettlers.logic.map.loading.newmap.FreshMapSerializer;
import jsettlers.logic.map.loading.newmap.FreshMapSerializer.IMapDataReceiver;
import jsettlers.mapcreator.data.MapDataDelta.HeightChange;
import jsettlers.mapcreator.data.MapDataDelta.LandscapeChange;
import jsettlers.mapcreator.data.MapDataDelta.ObjectAdder;
import jsettlers.mapcreator.data.MapDataDelta.ObjectRemover;
import jsettlers.mapcreator.data.MapDataDelta.ResourceChanger;
import jsettlers.mapcreator.data.MapDataDelta.StartPointSetter;
import jsettlers.mapcreator.data.objects.BuildingContainer;
import jsettlers.mapcreator.data.objects.MapObjectContainer;
import jsettlers.mapcreator.data.objects.MovableObjectContainer;
import jsettlers.mapcreator.data.objects.ObjectContainer;
import jsettlers.mapcreator.data.objects.ProtectContainer;
import jsettlers.mapcreator.data.objects.StackContainer;
import jsettlers.mapcreator.data.objects.StoneObjectContainer;
import jsettlers.mapcreator.data.objects.TreeObjectContainer;
import jsettlers.mapcreator.mapvalidator.tasks.error.ValidatePlayerStartPosition;
/**
* This is the map data of a map that is beeing created by the editor.
*
* @author michael
*/
public class MapData implements IMapData {
private final int width;
private final int height;
private final ELandscapeType[][] landscapes;
private final byte[][] heights;
private final ObjectContainer[][] objects;
private final EResourceType[][] resources;
private final byte[][] resourceAmount;
private final short[][] blockedPartitions;
private MapDataDelta undoDelta;
private int playerCount;
/**
* Start position of all player, will be converted to a border in ValidatePlayerStartPosition
*
* @see ValidatePlayerStartPosition
*/
private ShortPoint2D[] playerStarts;
private byte[][] lastPlayers;
private boolean[][] lastBorders;
private final boolean[][] doneBuffer;
private boolean[][] failpoints;
private final LandscapeFader fader = new LandscapeFader();
private IGraphicsBackgroundListener backgroundListener;
public MapData(int width, int height, int playerCount, ELandscapeType ground) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("width and height must be positive");
}
if (width > Short.MAX_VALUE || height > Short.MAX_VALUE) {
throw new IllegalArgumentException("width and height must be less than " + (Short.MAX_VALUE + 1));
}
if (playerCount <= 0 || playerCount > CommonConstants.MAX_PLAYERS) {
throw new IllegalArgumentException("Player count must be 1.." + CommonConstants.MAX_PLAYERS);
}
this.playerCount = playerCount;
this.playerStarts = new ShortPoint2D[playerCount];
for (int i = 0; i < playerCount; i++) {
playerStarts[i] = new ShortPoint2D(width / 2, height / 2);
}
this.width = width;
this.height = height;
this.landscapes = new ELandscapeType[width][height];
this.heights = new byte[width][height];
this.resourceAmount = new byte[width][height];
this.resources = new EResourceType[width][height];
this.objects = new ObjectContainer[width][height];
this.blockedPartitions = new short[width][height];
this.doneBuffer = new boolean[width][height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
landscapes[x][y] = ground;
resources[x][y] = EResourceType.FISH;
}
}
resetUndoDelta();
}
public MapData(IMapData data) {
this(data.getWidth(), data.getHeight(), data.getPlayerCount(), ELandscapeType.GRASS);
for (short x = 0; x < width; x++) {
for (short y = 0; y < height; y++) {
landscapes[x][y] = data.getLandscape(x, y);
heights[x][y] = data.getLandscapeHeight(x, y);
resourceAmount[x][y] = data.getResourceAmount(x, y);
resources[x][y] = data.getResourceType(x, y);
}
}
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (data.getMapObject(x, y) != null) {
placeObject(data.getMapObject(x, y), x, y);
}
}
}
for (int i = 0; i < data.getPlayerCount(); i++) {
playerStarts[i] = data.getStartPoint(i);
}
resetUndoDelta();
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
/**
* Fills an area with the given landscape type.
*
* @param type
* @param area
*/
public void fill(ELandscapeType type, IMapArea area) {
assert (isAllFalse(this.doneBuffer));
System.out.println("filling");
int ymin = Integer.MAX_VALUE;
int ymax = Integer.MIN_VALUE;
int xmin = Integer.MAX_VALUE;
int xmax = Integer.MIN_VALUE;
for (ShortPoint2D pos : area) {
short x = pos.x;
short y = pos.y;
if (contains(x, y)) {
if (setLandscape(x, y, type)) {
doneBuffer[x][y] = true;
if (x < xmin) {
xmin = x;
}
if (x > xmax) {
xmax = x;
}
if (y < ymin) {
ymin = y;
}
if (y > ymax) {
ymax = y;
}
}
}
}
if (ymin == Integer.MAX_VALUE) {
return; // nothing done
}
if (xmin > 0) {
xmin -= 1;
}
if (xmax < width - 1) {
xmax += 1;
}
if (ymin > 0) {
ymin -= 1;
}
if (ymax < width - 1) {
ymax += 1;
}
System.out.println("searching border tiles...");
Queue<FadeTask> tasks = new ConcurrentLinkedQueue<>();
for (int y = ymin; y < ymax; y++) {
for (int x = xmin; x < xmax; x++) {
// we cannot use done[x][y], because done flag is set for other tiles, too.
if (area.contains(new ShortPoint2D(x, y))) {
for (EDirection dir : EDirection.VALUES) {
int tx = x + dir.getGridDeltaX();
int ty = y + dir.getGridDeltaY();
if (contains(tx, ty) && !doneBuffer[tx][ty]) {
tasks.add(new FadeTask(tx, ty, type));
doneBuffer[tx][ty] = true;
if (tx < xmin) {
xmin = tx;
} else if (tx > xmax) {
xmax = tx;
}
if (ty < ymin) {
ymin = ty;
} else if (ty > ymax) {
ymax = ty;
}
}
}
}
}
}
System.out.println("Found " + tasks.size() + " tiles, starting to work on them...");
while (!tasks.isEmpty()) {
FadeTask task = tasks.poll();
assert contains(task.x, task.y);
ELandscapeType[] fade = fader.getLandscapesBetween(task.type, landscapes[task.x][task.y]);
if (fade == null || fade.length <= 2) {
continue; // nothing to do
}
ELandscapeType newLandscape = fade[1];
setLandscape(task.x, task.y, newLandscape);
for (EDirection dir : EDirection.VALUES) {
int nx = task.x + dir.getGridDeltaX();
int ny = task.y + dir.getGridDeltaY();
if (contains(nx, ny) && !doneBuffer[nx][ny]) {
tasks.add(new FadeTask(nx, ny, newLandscape));
doneBuffer[nx][ny] = true;
if (nx < xmin) {
xmin = nx;
} else if (nx > xmax) {
xmax = nx;
}
if (ny < ymin) {
ymin = ny;
} else if (ny > ymax) {
ymax = ny;
}
}
}
}
// reset done buffer
for (int y = ymin; y <= ymax; y++) {
for (int x = xmin; x <= xmax; x++) {
doneBuffer[x][y] = false;
}
}
assert (isAllFalse(this.doneBuffer));
}
private static boolean isAllFalse(boolean[][] doneBuffer2) {
for (boolean[] arr : doneBuffer2) {
for (boolean b : arr) {
if (b) {
return false;
}
}
}
return true;
}
public boolean contains(int tx, int ty) {
return tx >= 0 && tx < width && ty >= 0 && ty < height;
}
private static class FadeTask {
private final ELandscapeType type;
private final int y;
private final int x;
/**
* A task to set the landscape at a given point to the landscpae close to type.
*
* @param x
* The x position where to fade
* @param y
* The y position where to fade
* @param type
* The type of landscape to face (almoast) to.
*/
public FadeTask(int x, int y, ELandscapeType type) {
this.x = x;
this.y = y;
this.type = type;
}
@Override
public String toString() {
return "FadeTask[" + x + ", " + y + ", " + type + "]";
}
}
private boolean setLandscape(int x, int y, ELandscapeType type) {
if (objects[x][y] != null) {
if (!landscapeAllowsObjects(type)) {
return false;
}
if (objects[x][y] instanceof LandscapeConstraint) {
LandscapeConstraint constraint = (LandscapeConstraint) objects[x][y];
if (!constraint.getAllowedLandscapes().contains(type)) {
return false;
}
}
}
undoDelta.addLandscapeChange(x, y, landscapes[x][y]);
landscapes[x][y] = type;
if (backgroundListener != null) {
backgroundListener.backgroundChangedAt((short) x, (short) y);
}
return true;
}
public void setListener(IGraphicsBackgroundListener backgroundListener) {
this.backgroundListener = backgroundListener;
}
public void placeObject(MapObject object, int x, int y) {
ObjectContainer container;
ProtectContainer protector = ProtectContainer.getInstance();
Set<ELandscapeType> landscapes = null;
if (object instanceof MapTreeObject) {
container = TreeObjectContainer.getInstance();
} else if (object instanceof MapStoneObject) {
container = new StoneObjectContainer((MapStoneObject) object);
} else if (object instanceof MovableObject) {
container = new MovableObjectContainer((MovableObject) object, x, y);
} else if (object instanceof StackObject) {
container = new StackContainer((StackObject) object);
} else if (object instanceof BuildingObject) {
container = new BuildingContainer((BuildingObject) object, new ShortPoint2D(x, y));
landscapes = ((BuildingObject) object).getType().getGroundTypes();
protector = new ProtectLandscapeConstraint(((BuildingObject) object).getType());
} else if (object instanceof MapDecorationObject) {
container = new MapObjectContainer((MapDecorationObject) object);
} else {
return; // error!
}
boolean allowed = true;
ShortPoint2D start = new ShortPoint2D(x, y);
for (RelativePoint p : container.getProtectedArea()) {
ShortPoint2D abs = p.calculatePoint(start);
if (!contains(abs.x, abs.y) || objects[abs.x][abs.y] != null || !landscapeAllowsObjects(getLandscape(abs.x, abs.y))
|| (landscapes != null && !landscapes.contains(getLandscape(abs.x, abs.y)))) {
allowed = false;
}
}
if (allowed) {
for (RelativePoint p : container.getProtectedArea()) {
ShortPoint2D abs = p.calculatePoint(start);
objects[abs.x][abs.y] = protector;
undoDelta.removeObject(abs.x, abs.y);
}
objects[x][y] = container;
undoDelta.removeObject(x, y);
}
}
public void setHeight(int x, int y, int height) {
byte safeheight;
if (height >= Byte.MAX_VALUE) {
safeheight = Byte.MAX_VALUE;
} else if (height <= 0) {
safeheight = 0;
} else {
safeheight = (byte) height;
}
undoDelta.addHeightChange(x, y, heights[x][y]);
heights[x][y] = safeheight;
if (backgroundListener != null) {
backgroundListener.backgroundChangedAt((short) x, (short) y);
}
}
private static boolean landscapeAllowsObjects(ELandscapeType type) {
return !type.isWater() && type != ELandscapeType.SNOW && type != ELandscapeType.RIVER1 && type != ELandscapeType.RIVER2
&& type != ELandscapeType.RIVER3 && type != ELandscapeType.RIVER4 && type != ELandscapeType.MOOR;
}
@Override
public ELandscapeType getLandscape(int x, int y) {
return landscapes[x][y];
}
@Override
public MapObject getMapObject(int x, int y) {
ObjectContainer container = objects[x][y];
if (container != null) {
return container.getMapObject();
} else {
return null;
}
}
@Override
public byte getLandscapeHeight(int x, int y) {
return heights[x][y];
}
@Override
public ShortPoint2D getStartPoint(int player) {
return playerStarts[player];
}
public ObjectContainer getMapObjectContainer(int x, int y) {
return objects[x][y];
}
public IMovable getMovableContainer(int x, int y) {
ObjectContainer container = objects[x][y];
if (container instanceof IMovable) {
return (IMovable) container;
} else {
return null;
}
}
public void resetUndoDelta() {
undoDelta = new MapDataDelta();
}
public MapDataDelta getUndoDelta() {
return undoDelta;
}
/**
* Applys a map delta. Does not do checking, so use with care!
*
* @param delta
*/
public MapDataDelta apply(MapDataDelta delta) {
MapDataDelta inverse = new MapDataDelta();
// heights
HeightChange c = delta.getHeightChanges();
while (c != null) {
inverse.addHeightChange(c.x, c.y, heights[c.x][c.y]);
heights[c.x][c.y] = c.height;
backgroundListener.backgroundChangedAt(c.x, c.y);
c = c.next;
}
// landscape
LandscapeChange cl = delta.getLandscapeChanges();
while (cl != null) {
inverse.addLandscapeChange(cl.x, cl.y, landscapes[cl.x][cl.y]);
landscapes[cl.x][cl.y] = cl.landscape;
backgroundListener.backgroundChangedAt(cl.x, cl.y);
cl = cl.next;
}
// objects
ObjectRemover remove = delta.getRemoveObjects();
while (remove != null) {
inverse.addObject(remove.x, remove.y, objects[remove.x][remove.y]);
objects[remove.x][remove.y] = null;
remove = remove.next;
}
ObjectAdder adder = delta.getAddObjects();
while (adder != null) {
inverse.removeObject(adder.x, adder.y);
objects[adder.x][adder.y] = adder.obj;
adder = adder.next;
}
ResourceChanger res = delta.getChangeResources();
while (res != null) {
inverse.changeResource(res.x, res.y, resources[res.x][res.y], resourceAmount[res.x][res.y]);
resources[res.x][res.y] = res.type;
resourceAmount[res.x][res.y] = res.amount;
res = res.next;
}
// start points
StartPointSetter start = delta.getStartPoints();
while (start != null) {
inverse.setStartPoint(start.player, playerStarts[start.player]);
playerStarts[start.player] = start.pos;
start = start.next;
}
return inverse;
}
@Override
public int getPlayerCount() {
return playerCount;
}
/**
* Class to read serialized data
*/
private static final class MapDataReceiver implements IMapDataReceiver {
MapData data = null;
@Override
public void setPlayerStart(byte player, int x, int y) {
data.playerStarts[player] = new ShortPoint2D(x, y);
}
@Override
public void setMapObject(int x, int y, MapObject object) {
data.placeObject(object, x, y);
}
@Override
public void setLandscape(int x, int y, ELandscapeType type) {
data.landscapes[x][y] = type;
}
@Override
public void setHeight(int x, int y, byte height) {
data.heights[x][y] = height;
}
@Override
public void setDimension(int width, int height, int playerCount) {
data = new MapData(width, height, playerCount, ELandscapeType.GRASS);
}
@Override
public void setResources(int x, int y, EResourceType type, byte amount) {
data.resources[x][y] = type;
data.resourceAmount[x][y] = amount;
}
@Override
public void setBlockedPartition(int x, int y, short blockedPartition) {
data.blockedPartitions[x][y] = blockedPartition;
}
}
/**
* Read serialized file
*
* @param in
* input stream
* @return MapData
* @throws IOException
*/
public static MapData deserialize(InputStream in) throws IOException {
MapDataReceiver receiver = new MapDataReceiver();
FreshMapSerializer.deserialize(receiver, in);
return receiver.data;
}
public void deleteObject(int x, int y) {
ObjectContainer obj = objects[x][y];
if (obj instanceof ProtectContainer) {
} else if (obj != null) {
undoDelta.addObject(x, y, obj);
objects[x][y] = null;
ShortPoint2D start = new ShortPoint2D(x, y);
RelativePoint[] area = obj.getProtectedArea();
for (RelativePoint point : area) {
ShortPoint2D pos = point.calculatePoint(start);
if (contains(pos.x, pos.y)) {
undoDelta.addObject(pos.x, pos.y, objects[pos.x][pos.y]);
objects[pos.x][pos.y] = null;
}
}
}
}
public boolean isBorder(int x, int y) {
return lastBorders != null && lastBorders[x][y];
}
public byte getPlayer(int x, int y) {
return lastPlayers != null ? lastPlayers[x][y] : (byte) -1;
}
public void setPlayers(byte[][] lastPlayers) {
this.lastPlayers = lastPlayers;
}
public void setBorders(boolean[][] lastBorders) {
this.lastBorders = lastBorders;
}
/**
* Start position of a player, will be converted to a border in ValidatePlayerStartPosition
*
* @see ValidatePlayerStartPosition
*
* @param activePlayer
* Player
* @param pos
* Position
*/
public void setStartPoint(int activePlayer, ShortPoint2D pos) {
this.undoDelta.setStartPoint(activePlayer, playerStarts[activePlayer]);
this.playerStarts[activePlayer] = pos;
}
public void setFailpoints(boolean[][] failpoints) {
this.failpoints = failpoints;
}
public boolean isFailpoint(int x, int y) {
return failpoints != null && failpoints[x][y];
}
/**
* Set the maximum player count
*
* @param maxPlayer
* Min: 1, Max: CommonConstants.MAX_PLAYERS
*/
public void setMaxPlayers(short maxPlayer) {
if (maxPlayer <= 0 || maxPlayer > CommonConstants.MAX_PLAYERS) {
throw new IllegalArgumentException("Player count must be 1.." + CommonConstants.MAX_PLAYERS);
}
ShortPoint2D[] newPlayerStarts = new ShortPoint2D[maxPlayer];
for (int i = 0; i < maxPlayer; i++) {
newPlayerStarts[i] = i < playerCount ? playerStarts[i] : new ShortPoint2D(width / 2, height / 2);
}
this.playerCount = maxPlayer;
this.playerStarts = newPlayerStarts;
}
@Override
public EResourceType getResourceType(short x, short y) {
return resources[x][y];
}
@Override
public byte getResourceAmount(short x, short y) {
return resourceAmount[x][y];
}
public void addResource(int x, int y, EResourceType type, byte amount) {
if (resourceAmount[x][y] <= amount) {
this.undoDelta.changeResource(x, y, resources[x][y], resourceAmount[x][y]);
resourceAmount[x][y] = amount;
resources[x][y] = type;
}
}
public void decreaseResourceTo(int x, int y, byte amount) {
if (resourceAmount[x][y] > amount) {
this.undoDelta.changeResource(x, y, resources[x][y], resourceAmount[x][y]);
resourceAmount[x][y] = amount;
}
}
@Override
public short getBlockedPartition(short x, short y) {
return blockedPartitions[x][y];
}
/**
* This method must be called before the data is saved to execute actions that need to be done before that.
*/
public void doPreSaveActions() {
calculateBlockedPartitions();
}
private void calculateBlockedPartitions() {
MilliStopWatch watch = new MilliStopWatch();
BitSet notBlockedSet = new BitSet(width * height);
for (short y = 0; y < height; y++) {
for (short x = 0; x < width; x++) {
notBlockedSet.set(x + width * y, !landscapes[x][y].isBlocking);
}
}
PartitionCalculatorAlgorithm partitionCalculator = new PartitionCalculatorAlgorithm(0, 0, width, height, notBlockedSet,
IBlockingProvider.DEFAULT_IMPLEMENTATION);
partitionCalculator.calculatePartitions();
for (short y = 0; y < height; y++) {
for (short x = 0; x < width; x++) {
blockedPartitions[x][y] = partitionCalculator.getPartitionAt(x, y);
}
}
watch.stop("Calculating partitions needed");
System.out.println("found " + partitionCalculator.getNumberOfPartitions() + " partitions.");
}
public IPreviewImageDataSupplier getPreviewImageDataSupplier() {
return new IPreviewImageDataSupplier() {
@Override
public byte getLandscapeHeight(short x, short y) {
return heights[x][y];
}
@Override
public ELandscapeType getLandscape(short x, short y) {
return landscapes[x][y];
}
};
}
}