package nallar.tickthreading.minecraft;
import nallar.collections.ConcurrentIterableArrayList;
import nallar.collections.ConcurrentUnsafeIterableArrayList;
import nallar.collections.ContainedRemoveSet;
import nallar.tickthreading.Log;
import nallar.tickthreading.minecraft.commands.TPSCommand;
import nallar.tickthreading.minecraft.tickregion.EntityTickRegion;
import nallar.tickthreading.minecraft.tickregion.TickRegion;
import nallar.tickthreading.minecraft.tickregion.TileEntityTickRegion;
import nallar.tickthreading.util.CollectionsUtil;
import nallar.tickthreading.util.TableFormatter;
import nallar.unsafe.UnsafeUtil;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityCreature;
import net.minecraft.entity.IProjectile;
import net.minecraft.entity.monster.EntityMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.gen.ChunkProviderServer;
import java.util.*;
import java.util.concurrent.*;
public final class TickManager {
public static final int regionSize = 32;
public static final int regionSizePower = 31 - Integer.numberOfLeadingZeros(regionSize);
public boolean profilingEnabled = false;
private double averageTickLength = 0;
private long lastTickLength = 0;
private long lastStartTime = 0;
private static final int shuffleInterval = 3600;
private int shuffleCount;
private final WorldServer world;
public final ConcurrentUnsafeIterableArrayList<TileEntity> tileEntityList = new ConcurrentUnsafeIterableArrayList<TileEntity>();
public final ConcurrentUnsafeIterableArrayList<Entity> entityList = new ConcurrentUnsafeIterableArrayList<Entity>();
public Object tileEntityLock = new Object();
public Object entityLock = new Object();
private final Map<Integer, TileEntityTickRegion> tileEntityCallables = new HashMap<Integer, TileEntityTickRegion>();
private final Map<Integer, EntityTickRegion> entityCallables = new HashMap<Integer, EntityTickRegion>();
private final ConcurrentIterableArrayList<TickRegion> tickRegions = new ConcurrentIterableArrayList<TickRegion>();
private final ThreadManager threadManager;
private final Map<Class<?>, Integer> entityClassToCountMap = new ConcurrentHashMap<Class<?>, Integer>();
private final ConcurrentLinkedQueue<TickRegion> removalQueue = new ConcurrentLinkedQueue<TickRegion>();
public TickManager(WorldServer world, int threads) {
threadManager = new ThreadManager(threads, "Entities in " + Log.name(world));
this.world = world;
shuffleCount = world.rand.nextInt(shuffleInterval);
}
public TileEntityTickRegion getTileEntityRegion(int hashCode) {
return tileEntityCallables.get(hashCode);
}
@SuppressWarnings("NumericCastThatLosesPrecision")
private TileEntityTickRegion getOrCreateRegion(TileEntity tileEntity) {
int regionX = tileEntity.xCoord >> regionSizePower;
int regionZ = tileEntity.zCoord >> regionSizePower;
int hashCode = getHashCodeFromRegionCoords(regionX, regionZ);
TileEntityTickRegion callable = tileEntityCallables.get(hashCode);
if (callable == null) {
synchronized (tickRegions) {
callable = tileEntityCallables.get(hashCode);
if (callable == null) {
callable = new TileEntityTickRegion(world, this, regionX, regionZ);
tileEntityCallables.put(hashCode, callable);
tickRegions.add(callable);
}
}
}
return callable;
}
public EntityTickRegion getEntityRegion(int hashCode) {
return entityCallables.get(hashCode);
}
@SuppressWarnings("NumericCastThatLosesPrecision")
private EntityTickRegion getOrCreateRegion(Entity entity) {
int regionX = (entity.chunkCoordX << 4) >> regionSizePower;
int regionZ = (entity.chunkCoordZ << 4) >> regionSizePower;
int hashCode = getHashCodeFromRegionCoords(regionX, regionZ);
EntityTickRegion callable = entityCallables.get(hashCode);
if (callable == null) {
synchronized (tickRegions) {
callable = entityCallables.get(hashCode);
if (callable == null) {
callable = new EntityTickRegion(world, this, regionX, regionZ);
entityCallables.put(hashCode, callable);
tickRegions.add(callable);
}
}
}
return callable;
}
public static int getHashCode(TileEntity tileEntity) {
return getHashCode(tileEntity.xCoord, tileEntity.zCoord);
}
public static int getHashCode(Entity entity) {
return getHashCode(entity.chunkCoordX << 4, entity.chunkCoordZ << 4);
}
public static int getHashCode(int x, int z) {
return getHashCodeFromRegionCoords(x >> regionSizePower, z >> regionSizePower);
}
public static int getHashCodeFromRegionCoords(int x, int z) {
return x + (z << 16);
}
public void queueForRemoval(TickRegion tickRegion) {
removalQueue.add(tickRegion);
}
private void processChanges() {
TickRegion tickRegion;
while ((tickRegion = removalQueue.poll()) != null) {
if (tickRegion.isEmpty()) {
synchronized (tickRegions) {
if ((tickRegion instanceof EntityTickRegion ? entityCallables.remove(tickRegion.hashCode) : tileEntityCallables.remove(tickRegion.hashCode)) == tickRegion) {
tickRegions.remove(tickRegion);
tickRegion.onRemove();
}
}
}
}
}
public boolean add(TileEntity tileEntity, boolean newEntity) {
TileEntityTickRegion tileEntityTickRegion = getOrCreateRegion(tileEntity);
if (tileEntityTickRegion.add(tileEntity)) {
tileEntity.tickRegion = tileEntityTickRegion;
if (newEntity) {
synchronized (tileEntityLock) {
tileEntityList.add(tileEntity);
}
}
return true;
}
return false;
}
public boolean add(Entity entity, boolean newEntity) {
EntityTickRegion entityTickRegion = getOrCreateRegion(entity);
if (entityTickRegion.add(entity)) {
entity.tickRegion = entityTickRegion;
if (newEntity) {
synchronized (entityLock) {
entityList.add(entity);
}
synchronized (entityClassToCountMap) {
Class entityClass = entity.getClass();
Integer count = entityClassToCountMap.get(entityClass);
if (count == null) {
count = 0;
}
entityClassToCountMap.put(entityClass, count + 1);
}
}
return true;
}
return false;
}
public void batchRemoveEntities(HashSet<Entity> entities) {
entities = safeCopyClear(entities);
if (entities == null) {
return;
}
synchronized (entityLock) {
entityList.removeAll(entities);
}
ChunkProviderServer chunkProviderServer = world.theChunkProviderServer;
for (Entity entity : entities) {
if (entity == null) {
continue;
}
int x = entity.chunkCoordX;
int z = entity.chunkCoordZ;
if (entity.addedToChunk) {
Chunk chunk = chunkProviderServer.getChunkIfExists(x, z);
if (chunk != null) {
chunk.removeEntity(entity);
}
}
world.onEntityRemoved(entity);
EntityTickRegion tickRegion = entity.tickRegion;
if (tickRegion != null) {
tickRegion.remove(entity);
entity.tickRegion = null;
Class entityClass = entity.getClass();
synchronized (entityClassToCountMap) {
Integer count = entityClassToCountMap.get(entityClass);
if (count == null) {
throw new IllegalStateException("Removed an entity which should not have been in the entityList");
}
entityClassToCountMap.put(entityClass, count - 1);
}
}
}
}
public void batchRemoveTileEntities(HashSet<TileEntity> tileEntities) {
tileEntities = safeCopyClear(tileEntities);
if (tileEntities == null) {
return;
}
for (TileEntity tileEntity : tileEntities) {
TileEntityTickRegion tickRegion = tileEntity.tickRegion;
if (tickRegion != null) {
tickRegion.remove(tileEntity);
tileEntity.tickRegion = null;
tileEntity.onChunkUnload();
}
}
synchronized (tileEntityLock) {
tileEntityList.removeAll(tileEntities);
}
}
private static <T> HashSet<T> safeCopyClear(HashSet<T> c) {
synchronized (c) {
if (c.isEmpty()) {
return null;
}
HashSet<T> copy = new HashSet<T>(c);
c.clear();
return copy;
}
}
public void remove(TileEntity tileEntity) {
TileEntityTickRegion tileEntityTickRegion = tileEntity.tickRegion;
if (tileEntityTickRegion == null) {
tileEntityTickRegion = getOrCreateRegion(tileEntity);
}
tileEntityTickRegion.remove(tileEntity);
removed(tileEntity);
}
public void remove(Entity entity) {
EntityTickRegion entityTickRegion = entity.tickRegion;
if (entityTickRegion == null) {
entityTickRegion = getOrCreateRegion(entity);
}
entityTickRegion.remove(entity);
removed(entity);
}
public void removed(TileEntity tileEntity) {
tileEntity.tickRegion = null;
synchronized (tileEntityLock) {
tileEntityList.remove(tileEntity);
}
}
public void removed(Entity entity) {
boolean removed;
entity.tickRegion = null;
synchronized (entityLock) {
removed = entityList.remove(entity);
}
if (removed) {
Class entityClass = entity.getClass();
synchronized (entityClassToCountMap) {
Integer count = entityClassToCountMap.get(entityClass);
if (count == null) {
throw new IllegalStateException("Removed an entity which should not have been in the entityList");
}
entityClassToCountMap.put(entityClass, count - 1);
}
}
}
public void doTick() {
boolean previousProfiling = world.theProfiler.profilingEnabled;
lastStartTime = System.nanoTime();
threadManager.waitForCompletion();
if (previousProfiling) {
world.theProfiler.profilingEnabled = false;
}
threadManager.runDelayableList(tickRegions);
postTick();
if (previousProfiling) {
world.theProfiler.profilingEnabled = true;
}
}
private void postTick() {
lastTickLength = (threadManager.waitForCompletion() - lastStartTime);
averageTickLength = ((averageTickLength * 127) + lastTickLength) / 128;
if (!removalQueue.isEmpty()) {
threadManager.run(new Runnable() {
@Override
public void run() {
processChanges();
if (shuffleCount++ % shuffleInterval == 0) {
synchronized (tickRegions) {
Collections.shuffle(tickRegions);
if (tickRegions.removeAll(Collections.singleton(null))) {
Log.severe("Something broke, tickRegions for " + world.getName() + " contained null!");
}
}
MinecraftServer.runQueue.add(new FixDiscrepanciesTask());
}
}
});
}
}
public void unload() {
threadManager.stop();
synchronized (tickRegions) {
for (TickRegion tickRegion : tickRegions) {
tickRegion.die();
}
tickRegions.clear();
}
entityList.clear();
entityClassToCountMap.clear();
UnsafeUtil.clean(this);
}
public void fixDiscrepancies(StringBuilder sb) {
long startTime = System.nanoTime();
int fixed = 0;
int missingEntities = 0;
int missingTiles = 0;
int duplicateEntities = 0;
int duplicateTiles = 0;
int invalidTiles = 0;
int unloadedEntities = 0;
int unloadedTiles = 0;
ChunkProviderServer chunkProviderServer = world.theChunkProviderServer;
{
Set<Entity> contained = new HashSet<Entity>();
Set<Entity> toRemove = new ContainedRemoveSet<Entity>();
List<Entity> unloaded = new ArrayList<Entity>();
synchronized (entityLock) {
for (Entity e : entityList) {
if (add(e, false)) {
missingEntities++;
fixed++;
} else if (!contained.add(e)) {
toRemove.add(e);
duplicateEntities++;
fixed++;
} else if (e instanceof IProjectile || e instanceof EntityCreature || e instanceof EntityMob) {
synchronized (e) {
Chunk chunk = world.getChunkIfExists(e.chunkCoordX, e.chunkCoordZ);
if (chunk == null || !chunk.entityLists[e.chunkCoordY].contains(e)) {
unloaded.add(e);
unloadedEntities++;
}
}
}
}
for (Entity e : unloaded) {
remove(e);
}
entityList.removeAll(toRemove);
}
}
{
Set<TileEntity> contained = new HashSet<TileEntity>();
Set<TileEntity> toRemove = new ContainedRemoveSet<TileEntity>();
List<TileEntity> copy = new ArrayList<TileEntity>(tileEntityList.size());
synchronized (tileEntityLock) {
for (TileEntity te : tileEntityList) {
copy.add(te);
if (add(te, false)) {
missingTiles++;
fixed++;
}
if (!contained.add(te)) {
toRemove.add(te);
duplicateTiles++;
fixed++;
}
}
tileEntityList.removeAll(toRemove);
}
for (TileEntity te : copy) {
Chunk chunk;
boolean invalid = te.isInvalid();
if (te.yCoord < 0 || te.yCoord > 255) {
sb.append("TileEntity ").append(Log.toString(te)).append(" has an invalid y coordinate.\n");
invalid = true;
}
if (invalid || (chunk = chunkProviderServer.getChunkIfExists(te.xCoord >> 4, te.zCoord >> 4)) == null || chunk.getChunkBlockTileEntity(te.xCoord & 15, te.yCoord, te.zCoord & 15) != te) {
if (invalid) {
invalidTiles++;
sb.append("Removed ").append(Log.toString(te)).append(" as it is invalid.\n");
} else {
unloadedTiles++;
te.invalidate();
sb.append("Removed ").append(Log.toString(te)).append(" as it should have been unloaded.\n");
}
fixed++;
remove(te);
}
}
}
int totalSize = tickRegions.size();
int tESize = tileEntityCallables.size();
int eSize = entityCallables.size();
if (eSize + tESize != totalSize) {
sb.append("TickRegion list size mismatch, total: ").append(totalSize).append(", te: ").append(tESize).append(", e: ").append(eSize).append(", combined: ").append(tESize + eSize);
if (fixed != 0) {
sb.append('\n');
}
}
if (fixed != 0) {
sb.append("Found and fixed ").append(fixed).append(" discrepancies in tile/entity lists in ").append(Log.name(world))
.append("\ntiles - invalid: ").append(invalidTiles).append(", missing: ").append(missingTiles).append(", duplicate: ").append(duplicateTiles).append(", unloaded: ").append(unloadedTiles)
.append("\nentities - missing: ").append(missingEntities).append(", duplicate: ").append(duplicateEntities).append(", unloaded: ").append(unloadedEntities)
.append("\nTook ").append((System.nanoTime() - startTime) / 1000000l).append("ms");
}
}
public void recordStats(final TPSCommand.StatsHolder statsHolder) {
statsHolder.entities += entityList.size();
statsHolder.tileEntities += tileEntityList.size();
statsHolder.chunks += world.theChunkProviderServer.getLoadedChunkCount();
}
public void writeStats(TableFormatter tf, final TPSCommand.StatsHolder statsHolder) {
long timeTotal = 0;
double time = Double.NaN;
try {
long[] tickTimes = MinecraftServer.getServer().getTickTimes(world);
for (long tick : tickTimes) {
timeTotal += tick;
}
time = (timeTotal) / (double) tickTimes.length;
if (time == 0) {
time = 0.1;
}
} catch (NullPointerException ignored) {
}
int entities = entityList.size();
statsHolder.entities += entities;
int tileEntities = tileEntityList.size();
statsHolder.tileEntities += tileEntities;
int chunks = world.theChunkProviderServer.getLoadedChunkCount();
statsHolder.chunks += chunks;
tf
.row(Log.name(world))
.row(entities)
.row(tileEntities)
.row(chunks)
.row(world.playerEntities.size())
.row(TableFormatter.formatDoubleWithPrecision((time * 100f) / MinecraftServer.getTargetTickTime(), 2) + '%');
}
public TableFormatter writeDetailedStats(TableFormatter tf) {
@SuppressWarnings("MismatchedQueryAndUpdateOfStringBuilder")
StringBuilder stats = tf.sb;
stats.append("World: ").append(Log.name(world)).append('\n');
stats.append("---- Slowest tick regions ----").append('\n');
// TODO: Rewrite this
float averageAverageTickTime = 0;
float maxTickTime = 0;
SortedMap<Float, TickRegion> sortedTickCallables = new TreeMap<Float, TickRegion>();
synchronized (tickRegions) {
for (TickRegion tickRegion : tickRegions) {
float averageTickTime = tickRegion.getAverageTickTime();
averageAverageTickTime += averageTickTime;
sortedTickCallables.put(averageTickTime, tickRegion);
if (averageTickTime > maxTickTime) {
maxTickTime = averageTickTime;
}
}
Collection<TickRegion> var = sortedTickCallables.values();
TickRegion[] sortedTickCallablesArray = var.toArray(new TickRegion[var.size()]);
tf
.heading("")
.heading("X")
.heading("Z")
.heading("N")
.heading("Time");
for (int i = sortedTickCallablesArray.length - 1; i >= sortedTickCallablesArray.length - 7; i--) {
if (i >= 0 && sortedTickCallablesArray[i].getAverageTickTime() > 0.2) {
sortedTickCallablesArray[i].writeStats(tf);
}
}
tf.finishTable();
averageAverageTickTime /= tickRegions.size();
stats.append("\n---- World stats ----");
stats.append('\n').append(world.getChunkProvider().makeString());
stats.append("\nAverage tick time: ").append(averageAverageTickTime).append("ms");
stats.append("\nMax tick time: ").append(maxTickTime).append("ms");
stats.append("\nEffective tick time: ").append(lastTickLength / 1000000f).append("ms");
stats.append("\nAverage effective tick time: ").append((float) averageTickLength / 1000000).append("ms");
stats.append("\nGlobal TPS: ").append(TableFormatter.formatDoubleWithPrecision(MinecraftServer.getTPS(), 2));
}
return tf;
}
public TableFormatter writeEntityStats(TableFormatter tf) {
tf
.heading("Main")
.heading("Map")
.heading("Region")
.heading("Player");
tf
.row(entityList.size())
.row(getTotalEntityCountFromMap())
.row(getTotalEntityCountFromRegions())
.row(getEntityCount(EntityPlayer.class));
tf.finishTable();
return tf;
}
private int getTotalEntityCountFromRegions() {
int count = 0;
for (EntityTickRegion entityTickRegion : entityCallables.values()) {
count += entityTickRegion.size();
}
return count;
}
private int getTotalEntityCountFromMap() {
int count = 0;
for (Map.Entry<Class<?>, Integer> entry : entityClassToCountMap.entrySet()) {
count += entry.getValue();
}
return count;
}
public int getEntityCount(Class<?> clazz) {
int count = 0;
for (Map.Entry<Class<?>, Integer> entry : entityClassToCountMap.entrySet()) {
if (clazz.isAssignableFrom(entry.getKey())) {
count += entry.getValue();
}
}
return count;
}
public TableFormatter writeRegionDetails(final TableFormatter tf, final int hashCode) {
int x = 0;
int z = 0;
TileEntityTickRegion tileEntityTickRegion = getTileEntityRegion(hashCode);
if (tileEntityTickRegion != null) {
tileEntityTickRegion.dump(tf);
x = tileEntityTickRegion.regionX;
z = tileEntityTickRegion.regionZ;
}
EntityTickRegion entityTickRegion = getEntityRegion(hashCode);
if (entityTickRegion != null) {
entityTickRegion.dump(tf);
x = entityTickRegion.regionX;
z = entityTickRegion.regionZ;
}
if (entityTickRegion == null && tileEntityTickRegion == null) {
tf.sb.append("tickRegion for ").append(hashCode).append(" does not exist");
} else {
tf.sb.append("Dumped tickRegions for ").append(hashCode).append(": ").append(x).append(", ").append(z);
}
return tf;
}
public TableFormatter writeTECounts(final TableFormatter tf) {
final Map<Class, ComparableIntegerHolder> counts = new HashMap<Class, ComparableIntegerHolder>() {
@Override
public ComparableIntegerHolder get(Object key_) {
Class key = (Class) key_;
ComparableIntegerHolder value = super.get(key);
if (value == null) {
value = new ComparableIntegerHolder();
put(key, value);
}
return value;
}
};
for (TileEntity tileEntity : tileEntityList.unsafe()) {
if (tileEntity == null) {
continue;
}
counts.get(tileEntity.getClass()).value++;
}
List<Class> sortedKeys = CollectionsUtil.sortedKeys(counts, 15);
tf
.heading("Type")
.heading("Number");
for (Class clazz : sortedKeys) {
tf
.row(clazz.getName())
.row(counts.get(clazz).value);
}
tf.finishTable();
return tf;
}
private class ComparableIntegerHolder implements Comparable<ComparableIntegerHolder> {
public int value;
ComparableIntegerHolder() {
}
@Override
public int compareTo(final ComparableIntegerHolder comparableIntegerHolder) {
int otherValue = comparableIntegerHolder.value;
return (value < otherValue) ? -1 : ((value == otherValue) ? 0 : 1);
}
}
private class FixDiscrepanciesTask implements Runnable {
FixDiscrepanciesTask() {
}
@Override
public void run() {
StringBuilder sb = new StringBuilder();
fixDiscrepancies(sb);
if (sb.length() > 0) {
Log.severe(sb.toString());
}
}
}
}