/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion 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, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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.
*/
package illarion.client.world;
import illarion.client.IllaClient;
import illarion.client.graphics.QuestMarker;
import illarion.client.graphics.QuestMarker.QuestMarkerAvailability;
import illarion.client.graphics.QuestMarker.QuestMarkerType;
import illarion.client.gui.MiniMapGui;
import illarion.client.gui.MiniMapGui.Pointer;
import illarion.client.net.server.TileUpdate;
import illarion.client.world.interactive.InteractiveMap;
import illarion.common.config.ConfigChangedEvent;
import illarion.common.graphics.ItemInfo;
import illarion.common.types.Direction;
import illarion.common.types.ServerCoordinate;
import illarion.common.util.Stoppable;
import org.bushe.swing.event.annotation.AnnotationProcessor;
import org.bushe.swing.event.annotation.EventTopicPatternSubscriber;
import org.illarion.engine.Engine;
import org.illarion.engine.EngineException;
import org.illarion.engine.graphic.Color;
import org.illarion.engine.graphic.LightingMap;
import org.jetbrains.annotations.Contract;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
/**
* This handler stores all map data and ensures the updates of the map. This
* class is fully thread save for all actions. Clipping, hiding effects and map
* optimization is done by the GameMapProcessor.
*
* @author Martin Karing <nitram@illarion.org>
* @author Andreas Grob <vilarion@illarion.org>
*/
@ThreadSafe
public final class GameMap implements LightingMap, Stoppable {
private static final class QuestMarkerCarrier {
@Nullable
private final QuestMarker mapMarker;
@Nullable
private final Pointer guiMarker;
private QuestMarkerCarrier(@Nullable QuestMarker mapMarker, @Nullable Pointer guiMarker) {
this.mapMarker = mapMarker;
this.guiMarker = guiMarker;
}
@Nullable
@Contract(pure = true)
private Pointer getGuiMarker() {
return guiMarker;
}
@Nullable
@Contract(pure = true)
private QuestMarker getMapMarker() {
return mapMarker;
}
void setActiveQuest(boolean activeQuest) {
if (guiMarker != null) {
guiMarker.setCurrentQuest(activeQuest);
}
}
boolean isRemoved() {
return (mapMarker == null) || mapMarker.isMarkedAsRemoved();
}
void removeMarker() {
if (guiMarker != null) {
World.getGameGui().getMiniMapGui().releasePointer(guiMarker);
}
if (mapMarker != null) {
mapMarker.markAsRemoved();
}
}
}
/**
* The interactive map that is used to handle the player interaction with the map.
*/
@Nonnull
private final InteractiveMap interactive;
/**
* The lock that secures the map tiles.
*/
@Nonnull
private final ReadWriteLock mapLock;
/**
* The handler for the overview map.
*/
@Nonnull
private final GameMiniMap miniMap;
/**
* The tiles of the map. The key of the hash map is the location key of the tiles location.
*/
@Nonnull
@GuardedBy("mapLock")
private final Map<ServerCoordinate, MapTile> tiles;
/**
* This is the list of active quest markers that show where a quest starts.
*/
@Nonnull
private final Map<ServerCoordinate, QuestMarkerCarrier> activeQuestStartMarkers;
/**
* This is the list of active quest markers that show the target of a quest.
*/
@Nonnull
private final Map<ServerCoordinate, QuestMarkerCarrier> activeQuestTargetMarkers;
/**
* This is the list of quest markers that show the target of the quest, that are not visible on the map yet.
*/
@Nonnull
private final Map<ServerCoordinate, QuestMarkerCarrier> inactiveQuestTargetLocations;
/**
* This is set {@code true} in case the quest markers are supposed to be displayed on the mini map.
*/
private boolean showQuestsOnMiniMap;
/**
* This is set {@code true} in case the quest markers are supposed to be displayed on the game map.
*/
private boolean showQuestsOnGameMap;
/**
* Default constructor of the map handler.
*/
public GameMap(@Nonnull Engine engine) throws EngineException {
tiles = new HashMap<>();
interactive = new InteractiveMap(this);
activeQuestStartMarkers = new HashMap<>();
activeQuestTargetMarkers = new HashMap<>();
inactiveQuestTargetLocations = new HashMap<>();
mapLock = new ReentrantReadWriteLock();
miniMap = new GameMiniMap(engine);
showQuestsOnMiniMap = IllaClient.getCfg().getBoolean("showQuestsOnMiniMap");
showQuestsOnGameMap = IllaClient.getCfg().getBoolean("showQuestsOnGameMap");
AnnotationProcessor.process(this);
}
@EventTopicPatternSubscriber(topicPattern = "showQuestsOn.+Map")
public void onQuestMarkerSettingsChanged(@Nonnull String topic, @Nonnull ConfigChangedEvent event) {
if ("showQuestsOnMiniMap".equals(topic)) {
showQuestsOnMiniMap = event.getConfig().getBoolean("showQuestsOnMiniMap");
} else if ("showQuestsOnGameMap".equals(topic)) {
showQuestsOnGameMap = event.getConfig().getBoolean("showQuestsOnGameMap");
}
}
public void applyQuestTargetLocations(@Nonnull Iterable<ServerCoordinate> targets) {
//removeAllQuestMarkers();
Collection<ServerCoordinate> oldMarkers = new HashSet<>(activeQuestTargetMarkers.keySet());
oldMarkers.addAll(inactiveQuestTargetLocations.keySet());
for (@Nonnull ServerCoordinate markerLocation : targets) {
if (oldMarkers.contains(markerLocation)) {
oldMarkers.remove(markerLocation);
} else {
MapTile tile = getMapAt(markerLocation);
@Nullable Pointer targetPointer;
if (showQuestsOnMiniMap) {
targetPointer = World.getGameGui().getMiniMapGui().createTargetPointer();
targetPointer.setTarget(markerLocation);
targetPointer.setCurrentQuest(true);
World.getGameGui().getMiniMapGui().addPointer(targetPointer);
} else {
targetPointer = null;
}
if ((tile != null) && showQuestsOnGameMap) {
QuestMarker newMarker = new QuestMarker(QuestMarkerType.Target, tile);
newMarker.setAvailability(QuestMarkerAvailability.Available);
activeQuestTargetMarkers.put(markerLocation, new QuestMarkerCarrier(newMarker, targetPointer));
newMarker.show();
} else if (targetPointer != null) {
inactiveQuestTargetLocations.put(markerLocation, new QuestMarkerCarrier(null, targetPointer));
}
}
}
for (@Nonnull ServerCoordinate markerLocation : oldMarkers) {
QuestMarkerCarrier activeCarrier = activeQuestTargetMarkers.get(markerLocation);
if (activeCarrier != null) {
activeCarrier.setActiveQuest(false);
//activeCarrier.removeMarker();
}
QuestMarkerCarrier inactiveCarrier = inactiveQuestTargetLocations.remove(markerLocation);
if (inactiveCarrier != null) {
inactiveCarrier.removeMarker();
}
}
}
public void removeQuestMarkers(@Nonnull Iterable<ServerCoordinate> targets) {
Collection<ServerCoordinate> currentMarkers = new HashSet<>(activeQuestTargetMarkers.keySet());
currentMarkers.addAll(inactiveQuestTargetLocations.keySet());
currentMarkers.addAll(activeQuestStartMarkers.keySet());
for (@Nonnull ServerCoordinate markerLocation : targets) {
QuestMarkerCarrier activeStartCarrier = activeQuestStartMarkers.remove(markerLocation);
if (activeStartCarrier != null) {
activeStartCarrier.removeMarker();
}
QuestMarkerCarrier activeCarrier = activeQuestTargetMarkers.remove(markerLocation);
if (activeCarrier != null) {
activeCarrier.removeMarker();
}
QuestMarkerCarrier inactiveCarrier = inactiveQuestTargetLocations.remove(markerLocation);
if (inactiveCarrier != null) {
inactiveCarrier.removeMarker();
}
}
}
/**
* Apply the new quest source locations.
*
* @param available the list of available quests
* @param availableSoon the list of quests that will become available soon
*/
public void applyQuestStartLocations(
@Nonnull Iterable<ServerCoordinate> available, @Nonnull Iterable<ServerCoordinate> availableSoon) {
Collection<ServerCoordinate> currentMarkers = new HashSet<>(activeQuestStartMarkers.keySet());
for (int i = 0; i < 2; i++) {
QuestMarkerAvailability availability;
Iterable<ServerCoordinate> collection;
switch (i) {
case 0:
availability = QuestMarkerAvailability.Available;
collection = available;
break;
case 1:
availability = QuestMarkerAvailability.AvailableSoon;
collection = availableSoon;
break;
default:
continue;
}
for (@Nonnull ServerCoordinate markerLocation : collection) {
QuestMarkerCarrier carrier = activeQuestStartMarkers.get(markerLocation);
if (carrier != null) {
QuestMarker mapMarker = carrier.getMapMarker();
if ((mapMarker != null) && (mapMarker.getAvailability() != availability)) {
MiniMapGui gui = World.getGameGui().getMiniMapGui();
gui.releasePointer(carrier.getGuiMarker());
mapMarker.setAvailability(availability);
Pointer pointer = gui
.createStartPointer(availability == QuestMarkerAvailability.Available);
pointer.setTarget(markerLocation);
activeQuestStartMarkers.put(markerLocation, new QuestMarkerCarrier(mapMarker, pointer));
gui.addPointer(pointer);
}
currentMarkers.remove(markerLocation);
} else {
MapTile tile = getMapAt(markerLocation);
if (tile != null) {
MiniMapGui gui = World.getGameGui().getMiniMapGui();
@Nullable QuestMarker newMarker;
if (showQuestsOnGameMap) {
newMarker = new QuestMarker(QuestMarkerType.Start, tile);
newMarker.setAvailability(availability);
newMarker.show();
} else {
newMarker = null;
}
@Nullable Pointer pointer;
if (showQuestsOnMiniMap) {
pointer = gui
.createStartPointer(availability == QuestMarkerAvailability.Available);
pointer.setTarget(markerLocation);
gui.addPointer(pointer);
} else {
pointer = null;
}
activeQuestStartMarkers.put(markerLocation, new QuestMarkerCarrier(newMarker, pointer));
}
}
}
}
for (@Nonnull ServerCoordinate markerLocation : currentMarkers) {
QuestMarkerCarrier carrier = activeQuestStartMarkers.remove(markerLocation);
if (carrier != null) {
carrier.removeMarker();
}
}
}
/**
* Determines whether a map location accepts the light from a specific direction.
*
* @param coordinate the location of the tile
* @param dx the X-Delta of the light ray direction
* @param dy the Y-Delta of the light ray direction
* @return {@code true} if the position accepts the light, false if not
*/
@Override
public boolean acceptsLight(@Nonnull ServerCoordinate coordinate, int dx, int dy) {
MapTile tile = getMapAt(coordinate);
if (tile != null) {
switch (tile.getFace()) {
case ItemInfo.FACE_ALL:
return true;
case ItemInfo.FACE_W:
return dx >= 0;
case ItemInfo.FACE_SW:
return (dy - dx) < 0;
case ItemInfo.FACE_S:
return dy <= 0;
default:
return true;
}
}
return false;
}
/**
* Determines how much the tile blocks the view.
*
* @param coordinate the location of the tile
* @return obscurity of the tile, 0 for clear view {@link LightingMap#BLOCKED_VIEW} for fully blocked
*/
@Override
@Contract(pure = true)
public int blocksView(@Nonnull ServerCoordinate coordinate) {
MapTile tile = getMapAt(coordinate);
if (tile == null) {
return 0;
}
return tile.getCoverage();
}
/**
* Make the map processor checking if the player is inside a building or a
* cave or something else and start fading out that tiles.
*/
public void checkInside() {
GameMapProcessor2.checkInside();
}
/**
* Clear the entire map. This will cause all the tiles and items to be removed. It does not touch the characters.
*/
public void clear() {
MapTile[] oldTiles = null;
mapLock.writeLock().lock();
try {
oldTiles = tiles.values().toArray(new MapTile[tiles.size()]);
tiles.clear();
} finally {
mapLock.writeLock().unlock();
}
for (MapTile oldTile : oldTiles) {
oldTile.markAsRemoved();
}
for (@Nonnull Entry<ServerCoordinate, QuestMarkerCarrier> markers : activeQuestTargetMarkers.entrySet()) {
QuestMarker questMarker = markers.getValue().getMapMarker();
if (questMarker != null) {
questMarker.markAsRemoved();
}
inactiveQuestTargetLocations
.put(markers.getKey(), new QuestMarkerCarrier(null, markers.getValue().getGuiMarker()));
}
activeQuestTargetMarkers.clear();
for (@Nonnull Entry<ServerCoordinate, QuestMarkerCarrier> markers : activeQuestStartMarkers.entrySet()) {
markers.getValue().removeMarker();
}
activeQuestStartMarkers.clear();
}
/**
* Check if the map is currently empty.
*
* @return {@code true} in case the map is empty
*/
@Contract(pure = true)
public boolean isEmpty() {
return tiles.isEmpty();
}
/**
* Get the amount of tiles currently stored in the map.
*
* @return the amount of tiles
*/
@Contract(pure = true)
public int getTileCount() {
return tiles.size();
}
/**
* Get item elevation on a special position.
*
* @param loc the location that shall be checked
* @return the elevation value
*/
@Contract(pure = true)
public int getElevationAt(@Nonnull ServerCoordinate loc) {
MapTile ground = getMapAt(loc);
if (ground != null) {
return ground.getElevation();
}
return 0;
}
/**
* Get the interactive map that is used to interact with this map.
*
* @return the map used to interact with this map
*/
@Nonnull
@Contract(pure = true)
public InteractiveMap getInteractive() {
return interactive;
}
/**
* Get a map tile at a specified location.
*
* @param coordinate the coordinates of the location
* @return the map tile at the location or {@code null}
*/
@Nullable
@Contract(pure = true)
public MapTile getMapAt(@Nonnull ServerCoordinate coordinate) {
mapLock.readLock().lock();
try {
return tiles.get(coordinate);
} finally {
mapLock.readLock().unlock();
}
}
/**
* Get the overview map handler that is used currently.
*
* @return the object that handles the overview map
*/
@Nonnull
@Contract(pure = true)
public GameMiniMap getMiniMap() {
return miniMap;
}
/**
* Remove a tile by its key from the map.
*
* @param coordinate the coordinate of the tile that is to be removed
*/
public boolean removeTile(ServerCoordinate coordinate) {
@Nullable MapTile removedTile = null;
mapLock.writeLock().lock();
try {
removedTile = tiles.remove(coordinate);
} finally {
mapLock.writeLock().unlock();
}
if (removedTile != null) {
@Nullable QuestMarkerCarrier marker = activeQuestTargetMarkers.remove(coordinate);
if (marker != null) {
QuestMarker questMarker = marker.getMapMarker();
if (questMarker != null) {
questMarker.markAsRemoved();
}
Pointer guiMarker = marker.getGuiMarker();
if (guiMarker != null) {
inactiveQuestTargetLocations.put(coordinate, new QuestMarkerCarrier(null, guiMarker));
}
}
@Nullable QuestMarkerCarrier startMarker = activeQuestStartMarkers.remove(coordinate);
if (startMarker != null) {
startMarker.removeMarker();
}
removedTile.markAsRemoved();
return true;
}
return false;
}
/**
* Render lights based on the tile light and the ambient light generated by the current IG time and the weather.
*/
@Override
public void renderLights() {
mapLock.writeLock().lock();
try {
Color ambientLight = World.getWeather().getAmbientLight();
for (MapTile tile : tiles.values()) {
tile.renderLight();
tile.applyAmbientLight(ambientLight);
}
} finally {
mapLock.writeLock().unlock();
}
World.getPeople().updateLight();
}
public void updateAmbientLight() {
mapLock.writeLock().lock();
try {
Color ambientLight = World.getWeather().getAmbientLight();
for (MapTile tile : tiles.values()) {
tile.applyAmbientLight(ambientLight);
}
} finally {
mapLock.writeLock().unlock();
}
World.getPeople().updateLight();
}
@Override
public void saveShutdown() {
clear();
miniMap.saveShutdown();
}
/**
* Set a light color on a tile.
*
* @param coordinate the location of the map tile on the server map
* @param color the color that shall be set for this tile
*/
@Override
public void setLight(@Nonnull ServerCoordinate coordinate, @Nonnull Color color) {
MapTile tile = getMapAt(coordinate);
if (tile != null) {
tile.addLight(color);
}
}
/**
* This function sends all tiles to the map processor and causes it to check the tiles again.
*/
public void updateAllTiles() {
Collection<ServerCoordinate> tilesToDelete = new HashSet<>();
mapLock.readLock().lock();
try {
tilesToDelete.addAll(tiles.values().stream().filter(GameMapProcessor2::isOutsideOfClipping).map(MapTile::getCoordinates).collect(Collectors.toList()));
} finally {
mapLock.readLock().unlock();
}
if (!tilesToDelete.isEmpty()) {
mapLock.writeLock().lock();
try {
tilesToDelete.forEach(this::removeTile);
} finally {
mapLock.writeLock().unlock();
}
}
}
public void updateTiles(@Nonnull Iterable<TileUpdate> updateDataList) {
mapLock.writeLock().lock();
try {
for (@Nonnull TileUpdate updateData : updateDataList) {
updateTile(updateData);
}
} finally {
mapLock.writeLock().unlock();
}
}
/**
* Perform a update of a single map tile regarding the update information. This can add a new tile,
* update a old one or delete one tile.
*
* @param updateData the data of the update
*/
public void updateTile(@Nonnull TileUpdate updateData) {
boolean changedSomething = false;
ServerCoordinate coordinate = updateData.getLocation();
if (updateData.getTileId() == MapTile.ID_NONE) {
changedSomething = removeTile(coordinate);
} else {
MapTile tile = getMapAt(coordinate);
boolean newTile = tile == null;
// create a tile for this location if none was found
if (newTile) {
tile = new MapTile(updateData.getLocation());
}
// update tile from update info
if (tile.update(updateData)) {
changedSomething = true;
}
if (newTile) {
tile.applyAmbientLight(World.getWeather().getAmbientLight());
setColorLinks(tile);
GameMapProcessor2.processTile(tile);
mapLock.writeLock().lock();
try {
tiles.put(coordinate, tile);
} finally {
mapLock.writeLock().unlock();
}
QuestMarkerCarrier inactiveMarker = inactiveQuestTargetLocations.remove(updateData.getLocation());
if (inactiveMarker != null) {
Pointer pointer = inactiveMarker.getGuiMarker();
QuestMarker newMarker = new QuestMarker(QuestMarkerType.Target, tile);
newMarker.setAvailability(QuestMarkerAvailability.Available);
activeQuestTargetMarkers
.put(updateData.getLocation(), new QuestMarkerCarrier(newMarker, pointer));
newMarker.show();
}
changedSomething = true;
}
if (changedSomething) {
if (World.getMapDisplay().isActive()) {
World.getLights().notifyChange(updateData.getLocation());
}
if (World.getPlayer().getLocation().equals(updateData.getLocation())) {
World.getMusicBox().updatePlayerLocation();
}
}
}
if (changedSomething) {
miniMap.update(updateData);
}
}
@Nullable
@Contract(pure = true)
private MapTile getMapAt(@Nonnull ServerCoordinate origin, @Nonnull Direction direction) {
return getMapAt(new ServerCoordinate(origin, direction));
}
/**
* This function is used to establish the links between the color values of the different tiles.
*
* @param tile the new tile that is added
*/
private void setColorLinks(@Nonnull MapTile tile) {
ServerCoordinate tileLocation = tile.getCoordinates();
mapLock.readLock().lock();
try {
//noinspection ConstantConditions
for (Direction dir : Direction.values()) {
MapTile offsetTile = getMapAt(tileLocation, dir);
if (offsetTile != null) {
tile.linkColors(offsetTile, dir);
}
}
} finally {
mapLock.readLock().unlock();
}
}
}