package hunternif.mc.atlas.marker;
import hunternif.mc.atlas.api.MarkerAPI;
import hunternif.mc.atlas.network.PacketDispatcher;
import hunternif.mc.atlas.network.client.MarkersPacket;
import hunternif.mc.atlas.registry.MarkerRegistry;
import hunternif.mc.atlas.registry.MarkerType;
import hunternif.mc.atlas.util.Log;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.world.WorldSavedData;
import net.minecraftforge.common.util.Constants;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Contains markers, mapped to dimensions, and then to their chunk coordinates.
* <p>
* On the server a separate instance of MarkersData contains all the global
* markers, which are also copied to atlases, but not saved with them.
* At runtime clients have both types of markers in the same collection..
* </p>
* @author Hunternif
*/
public class MarkersData extends WorldSavedData {
private static final int VERSION = 3;
private static final String TAG_VERSION = "aaVersion";
private static final String TAG_DIMENSION_MAP_LIST = "dimMap";
private static final String TAG_DIMENSION_ID = "dimID";
private static final String TAG_MARKERS = "markers";
private static final String TAG_MARKER_ID = "id";
private static final String TAG_MARKER_TYPE = "markerType";
private static final String TAG_MARKER_LABEL = "label";
private static final String TAG_MARKER_X = "x";
private static final String TAG_MARKER_Y = "y";
private static final String TAG_MARKER_VISIBLE_AHEAD = "visAh";
/** Markers are stored in lists within square areas this many MC chunks
* across. */
public static final int CHUNK_STEP = 8;
/** Set of players this data has been sent to, only once after they connect. */
private final Set<EntityPlayer> playersSentTo = new HashSet<>();
private final AtomicInteger largestID = new AtomicInteger(0);
private int getNewID() {
return largestID.incrementAndGet();
}
private final Map<Integer /*marker ID*/, Marker> idMap = new ConcurrentHashMap<>(2, 0.75f, 2);
/**
* Maps a list of markers in a square to the square's coordinates, then to
* dimension ID. It exists in case someone needs to quickly find markers
* located in a square.
* Within the list markers are ordered by the Z coordinate, so that markers
* placed closer to the south will appear in front of those placed closer to
* the north.
* TODO: consider using Quad-tree. At small zoom levels iterating through
* chunks to render markers gets very slow.
*/
private final Map<Integer /*dimension ID*/, DimensionMarkersData> dimensionMap =
new ConcurrentHashMap<>(2, 0.75f, 2);
public MarkersData(String key) {
super(key);
}
@Override
public void readFromNBT(NBTTagCompound compound) {
int version = compound.getInteger(TAG_VERSION);
if (version < VERSION) {
Log.warn("Outdated atlas data format! Was %d but current is %d", version, VERSION);
this.markDirty();
}
NBTTagList dimensionMapList = compound.getTagList(TAG_DIMENSION_MAP_LIST, Constants.NBT.TAG_COMPOUND);
for (int d = 0; d < dimensionMapList.tagCount(); d++) {
NBTTagCompound tag = dimensionMapList.getCompoundTagAt(d);
int dimensionID = tag.getInteger(TAG_DIMENSION_ID);
NBTTagList tagList = tag.getTagList(TAG_MARKERS, Constants.NBT.TAG_COMPOUND);
for (int i = 0; i < tagList.tagCount(); i++) {
NBTTagCompound markerTag = tagList.getCompoundTagAt(i);
boolean visibleAhead = true;
if (version < 2) {
Log.warn("Marker is visible ahead by default");
} else {
visibleAhead = markerTag.getBoolean(TAG_MARKER_VISIBLE_AHEAD);
}
int id;
if (version < 3) {
id = getNewID();
} else {
id = markerTag.getInteger(TAG_MARKER_ID);
if (getMarkerByID(id) != null) {
Log.warn("Loading marker with duplicate id %d. Getting new id", id);
id = getNewID();
}
this.markDirty();
}
if (largestID.intValue() < id) {
largestID.set(id);
}
Marker marker = new Marker(
id,
MarkerRegistry.find(markerTag.getString(TAG_MARKER_TYPE)),
markerTag.getString(TAG_MARKER_LABEL),
dimensionID,
markerTag.getInteger(TAG_MARKER_X),
markerTag.getInteger(TAG_MARKER_Y),
visibleAhead);
loadMarker(marker);
}
}
}
@Override
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
Log.info("Saving local markers data to NBT");
compound.setInteger(TAG_VERSION, VERSION);
NBTTagList dimensionMapList = new NBTTagList();
for (Integer dimension : dimensionMap.keySet()) {
NBTTagCompound tag = new NBTTagCompound();
tag.setInteger(TAG_DIMENSION_ID, dimension);
DimensionMarkersData data = getMarkersDataInDimension(dimension);
NBTTagList tagList = new NBTTagList();
for (Marker marker : data.getAllMarkers()) {
Log.debug("Saving marker %s", marker.toString());
NBTTagCompound markerTag = new NBTTagCompound();
markerTag.setInteger(TAG_MARKER_ID, marker.getId());
markerTag.setString(TAG_MARKER_TYPE, marker.getType().getRegistryName().toString());
markerTag.setString(TAG_MARKER_LABEL, marker.getLabel());
markerTag.setInteger(TAG_MARKER_X, marker.getX());
markerTag.setInteger(TAG_MARKER_Y, marker.getZ());
markerTag.setBoolean(TAG_MARKER_VISIBLE_AHEAD, marker.isVisibleAhead());
tagList.appendTag(markerTag);
}
tag.setTag(TAG_MARKERS, tagList);
dimensionMapList.appendTag(tag);
}
compound.setTag(TAG_DIMENSION_MAP_LIST, dimensionMapList);
return compound;
}
public Set<Integer> getVisitedDimensions() {
return dimensionMap.keySet();
}
/** This method is rather inefficient, use it sparingly. */
public Collection<Marker> getMarkersInDimension(int dimension) {
return getMarkersDataInDimension(dimension).getAllMarkers();
}
/** Creates a new instance of {@link DimensionMarkersData}, if necessary. */
public DimensionMarkersData getMarkersDataInDimension(int dimension) {
return dimensionMap.computeIfAbsent(dimension, k -> new DimensionMarkersData(this, dimension));
}
/** The "chunk" here is {@link MarkersData#CHUNK_STEP} times larger than the
* Minecraft 16x16 chunk! May return null. */
public List<Marker> getMarkersAtChunk(int dimension, int x, int z) {
return getMarkersDataInDimension(dimension).getMarkersAtChunk(x, z);
}
private Marker getMarkerByID(int id) {
return idMap.get(id);
}
public Marker removeMarker(int id) {
Marker marker = getMarkerByID(id);
if (marker == null) return null;
if (idMap.remove(id) != null) {
getMarkersDataInDimension(marker.getDimension()).removeMarker(marker);
markDirty();
}
return marker;
}
/** For internal use. Use the {@link MarkerAPI} to put markers! This method
* creates a new marker from the given data, saves and returns it.
* Server side only! */
public Marker createAndSaveMarker(MarkerType type, String label, int dimension, int x, int z, boolean visibleAhead) {
Marker marker = new Marker(getNewID(), type, label, dimension, x, z, visibleAhead);
Log.info("Created new marker %s", marker.toString());
idMap.put(marker.getId(), marker);
getMarkersDataInDimension(marker.getDimension()).insertMarker(marker);
markDirty();
return marker;
}
/**
* For internal use, when markers are loaded from NBT or sent from the
* server. IF a marker's id is conflicting, the marker will not load!
* @return the marker instance that was added.
*/
public Marker loadMarker(Marker marker) {
if (!idMap.containsKey(marker.getId())) {
idMap.put(marker.getId(), marker);
getMarkersDataInDimension(marker.getDimension()).insertMarker(marker);
}
return marker;
}
public boolean isSyncedOnPlayer(EntityPlayer player) {
return playersSentTo.contains(player);
}
/** Send all data to the player in several packets. Called once during the
* first run of ItemAtals.onUpdate(). */
public void syncOnPlayer(int atlasID, EntityPlayer player) {
for (Integer dimension : dimensionMap.keySet()) {
MarkersPacket packet = newMarkersPacket(atlasID, dimension);
DimensionMarkersData data = getMarkersDataInDimension(dimension);
for (Marker marker : data.getAllMarkers()) {
packet.putMarker(marker);
}
PacketDispatcher.sendTo(packet, (EntityPlayerMP) player);
}
Log.info("Sent markers data #%d to player %s", atlasID, player.getName());
playersSentTo.add(player);
}
/** To be overridden in GlobalMarkersData. */
MarkersPacket newMarkersPacket(int atlasID, int dimension) {
return new MarkersPacket(atlasID, dimension);
}
public boolean isEmpty() {
return idMap.isEmpty();
}
}