package hunternif.mc.atlas.core;
import hunternif.mc.atlas.AntiqueAtlasMod;
import hunternif.mc.atlas.network.PacketDispatcher;
import hunternif.mc.atlas.network.client.MapDataPacket;
import hunternif.mc.atlas.network.server.BrowsingPositionPacket;
import hunternif.mc.atlas.util.Log;
import hunternif.mc.atlas.util.ShortVec2;
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.util.math.MathHelper;
import net.minecraft.world.WorldSavedData;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.common.util.Constants;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
public class AtlasData extends WorldSavedData {
private static final int VERSION = 2;
private static final String TAG_VERSION = "aaVersion";
private static final String TAG_DIMENSION_MAP_LIST = "qDimensionMap";
private static final String TAG_DIMENSION_ID = "qDimensionID";
private static final String TAG_VISITED_CHUNKS = "qVisitedChunks";
// Navigation
private static final String TAG_BROWSING_X = "qBrowseX";
private static final String TAG_BROWSING_Y = "qBrowseY";
private static final String TAG_BROWSING_ZOOM = "qBrowseZoom";
/** Maps dimension ID to biomeAnalyzer. */
private final Map<Integer, IBiomeDetector> biomeAnalyzers = new HashMap<>();
private final BiomeDetectorBase biomeDetectorOverworld = new BiomeDetectorBase();
private final BiomeDetectorNether biomeDetectorNether = new BiomeDetectorNether();
private final BiomeDetectorEnd biomeDetectorEnd = new BiomeDetectorEnd();
/** This map contains, for each dimension, a map of chunks the player
* has seen. This map is thread-safe.
* CAREFUL! Don't modify chunk coordinates that are already put in the map! */
private final Map<Integer /*dimension ID*/, DimensionData> dimensionMap =
new ConcurrentHashMap<>(2, 0.75f, 2);
/** Set of players this Atlas data has been sent to. */
private final Set<EntityPlayer> playersSentTo = new HashSet<>();
private NBTTagCompound nbt;
public AtlasData(String key) {
super(key);
biomeDetectorOverworld.setScanPonds(AntiqueAtlasMod.settings.doScanPonds);
setBiomeDetectorForDimension(0, biomeDetectorOverworld);
setBiomeDetectorForDimension(-1, biomeDetectorNether);
setBiomeDetectorForDimension(1, biomeDetectorEnd);
}
@Override
public void readFromNBT(NBTTagCompound compound) {
this.nbt = 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 dimTag = dimensionMapList.getCompoundTagAt(d);
int dimensionID = dimTag.getInteger(TAG_DIMENSION_ID);
int[] intArray = dimTag.getIntArray(TAG_VISITED_CHUNKS);
DimensionData dimData = getDimensionData(dimensionID);
for (int i = 0; i < intArray.length; i += 3) {
dimData.setTile(intArray[i], intArray[i+1], new Tile(intArray[i+2]));
}
double zoom = (double)dimTag.getInteger(TAG_BROWSING_ZOOM) / BrowsingPositionPacket.ZOOM_SCALE_FACTOR;
if (zoom == 0) {
zoom = 0.5;
}
dimData.setBrowsingPosition(dimTag.getInteger(TAG_BROWSING_X),
dimTag.getInteger(TAG_BROWSING_Y), zoom);
}
}
@Override
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound.setInteger(TAG_VERSION, VERSION);
NBTTagList dimensionMapList = new NBTTagList();
for (Entry<Integer, DimensionData> dimensionEntry : dimensionMap.entrySet()) {
NBTTagCompound dimTag = new NBTTagCompound();
dimTag.setInteger(TAG_DIMENSION_ID, dimensionEntry.getKey());
DimensionData dimData = dimensionEntry.getValue();
Map<ShortVec2, Tile> seenChunks = dimData.getSeenChunks();
int[] intArray = new int[seenChunks.size()*3];
int i = 0;
for (Entry<ShortVec2, Tile> entry : seenChunks.entrySet()) {
intArray[i++] = entry.getKey().x;
intArray[i++] = entry.getKey().y;
intArray[i++] = entry.getValue().biomeID;
}
dimTag.setIntArray(TAG_VISITED_CHUNKS, intArray);
dimTag.setInteger(TAG_BROWSING_X, dimData.getBrowsingX());
dimTag.setInteger(TAG_BROWSING_Y, dimData.getBrowsingY());
dimTag.setInteger(TAG_BROWSING_ZOOM, (int)Math.round(dimData.getBrowsingZoom() * BrowsingPositionPacket.ZOOM_SCALE_FACTOR));
dimensionMapList.appendTag(dimTag);
}
compound.setTag(TAG_DIMENSION_MAP_LIST, dimensionMapList);
return compound;
}
private void setBiomeDetectorForDimension(int dimension, IBiomeDetector biomeAnalyzer) {
biomeAnalyzers.put(dimension, biomeAnalyzer);
}
/** If not found, returns the analyzer for overworld. */
private IBiomeDetector getBiomeDetectorForDimension(int dimension) {
IBiomeDetector biomeAnalyzer = biomeAnalyzers.get(dimension);
return biomeAnalyzer == null ? biomeDetectorOverworld : biomeAnalyzer;
}
public void updateMapAroundPlayer(EntityPlayer player) {
// Update the actual map only so often:
int newScanInterval = Math.round(AntiqueAtlasMod.settings.newScanInterval * 20);
int rescanInterval = newScanInterval * AntiqueAtlasMod.settings.rescanRate;
if (player.ticksExisted % newScanInterval != 0) {
return;
}
int playerX = MathHelper.floor(player.posX) >> 4;
int playerZ = MathHelper.floor(player.posZ) >> 4;
ITileStorage seenChunks = this.getDimensionData(player.dimension);
IBiomeDetector biomeDetector = getBiomeDetectorForDimension(player.dimension);
int scanRadius = AntiqueAtlasMod.settings.scanRadius;
// Look at chunks around in a circular area:
for (double dx = -scanRadius; dx <= scanRadius; dx++) {
for (double dz = -scanRadius; dz <= scanRadius; dz++) {
if (dx*dx + dz*dz > scanRadius*scanRadius) {
continue; // Outside the circle
}
int x = (int)(playerX + dx);
int z = (int)(playerZ + dz);
Tile oldTile = seenChunks.getTile(x, z);
// Check if there's a custom tile at the location:
int biomeId = AntiqueAtlasMod.extBiomeData.getData().getBiomeIdAt(player.dimension, x, z);
// Custom tiles overwrite even the chunks already seen.
// If there's no custom tile, check the actual chunk:
if (biomeId == -1) {
Chunk chunk = player.getEntityWorld().getChunkFromChunkCoords(x, z);
// Force loading of chunk, if required:
if (AntiqueAtlasMod.settings.forceChunkLoading && !chunk.isLoaded()) {
player.getEntityWorld().getChunkProvider().provideChunk(x << 4, z << 4);
}
// Skip chunk if it hasn't loaded yet:
if (!chunk.isLoaded()) {
continue;
}
if (oldTile != null) {
// If the chunk has been scanned previously, only re-scan it so often:
if (!AntiqueAtlasMod.settings.doRescan || player.ticksExisted % rescanInterval != 0) {
continue;
}
biomeId = biomeDetector.getBiomeID(chunk);
if (biomeId == IBiomeDetector.NOT_FOUND) {
// If the new tile is empty, remove the old one:
this.removeTile(player.dimension, x, z);
} else if (oldTile.biomeID != biomeId) {
// Only update if the old tile's biome ID doesn't match the new one:
this.setTile(player.dimension, x, z, new Tile(biomeId));
}
} else {
// Scanning new chunk:
biomeId = biomeDetector.getBiomeID(chunk);
if (biomeId != IBiomeDetector.NOT_FOUND) {
this.setTile(player.dimension, x, z, new Tile(biomeId));
}
}
} else {
// Only update the custom tile if it doesn't rewrite itself:
if (oldTile == null || oldTile.biomeID != biomeId) {
this.setTile(player.dimension, x, z, new Tile(biomeId));
this.markDirty();
}
}
}
}
}
/** Puts a given tile into given map at specified coordinates and,
* if tileStitcher is present, sets appropriate sectors on adjacent tiles. */
public void setTile(int dimension, int x, int y, Tile tile) {
DimensionData dimData = getDimensionData(dimension);
dimData.setTile(x, y, tile);
}
/** Returns the Tile previously set at given coordinates. */
private Tile removeTile(int dimension, int x, int y) {
DimensionData dimData = getDimensionData(dimension);
return dimData.removeTile(x, y);
}
public Set<Integer> getVisitedDimensions() {
return dimensionMap.keySet();
}
/** If this dimension is not yet visited, empty DimensionData will be created. */
public DimensionData getDimensionData(int dimension) {
return dimensionMap.computeIfAbsent(dimension, k -> new DimensionData(this, dimension));
}
public Map<ShortVec2, Tile> getSeenChunksInDimension(int dimension) {
return getDimensionData(dimension).getSeenChunks();
}
/** The set of players this AtlasData has already been sent to. */
public Collection<EntityPlayer> getSyncedPlayers() {
return Collections.unmodifiableCollection(playersSentTo);
}
/** Whether this AtlasData has already been sent to the specified player. */
public boolean isSyncedOnPlayer(EntityPlayer player) {
return playersSentTo.contains(player);
}
/** Send all data to the player in several zipped packets. Called once
* during the first run of ItemAtals.onUpdate(). */
public void syncOnPlayer(int atlasID, EntityPlayer player) {
if (nbt == null) {
nbt = new NBTTagCompound();
}
// Before syncing make sure the changes are written to the nbt:
writeToNBT(nbt);
PacketDispatcher.sendTo(new MapDataPacket(atlasID, nbt), (EntityPlayerMP) player);
Log.info("Sent Atlas #%d data to player %s", atlasID, player.getName());
playersSentTo.add(player);
}
public boolean isEmpty() {
return dimensionMap.isEmpty();
}
}