package nallar.tickthreading.minecraft;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import cpw.mods.fml.common.IPlayerTracker;
import cpw.mods.fml.common.IScheduledTickHandler;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.TickType;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.event.FMLServerStartingEvent;
import cpw.mods.fml.common.network.NetworkMod;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.common.registry.TickRegistry;
import cpw.mods.fml.relauncher.Side;
import nallar.collections.IntSet;
import nallar.reporting.LeakDetector;
import nallar.reporting.Metrics;
import nallar.tickthreading.Log;
import nallar.tickthreading.minecraft.commands.Command;
import nallar.tickthreading.minecraft.commands.DumpCommand;
import nallar.tickthreading.minecraft.commands.ProfileCommand;
import nallar.tickthreading.minecraft.commands.TPSCommand;
import nallar.tickthreading.minecraft.commands.TicksCommand;
import nallar.tickthreading.minecraft.entitylist.EntityList;
import nallar.tickthreading.minecraft.entitylist.LoadedEntityList;
import nallar.tickthreading.minecraft.entitylist.LoadedTileEntityList;
import nallar.tickthreading.minecraft.profiling.EntityTickProfiler;
import nallar.tickthreading.util.ReflectUtil;
import nallar.tickthreading.util.TableFormatter;
import nallar.tickthreading.util.VersionUtil;
import nallar.tickthreading.util.contextaccess.ContextAccess;
import net.minecraft.command.ServerCommandManager;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraft.network.NetServerHandler;
import net.minecraft.network.packet.PacketCount;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.common.Configuration;
import net.minecraftforge.common.DimensionManager;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.EventPriority;
import net.minecraftforge.event.ForgeSubscribe;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.world.WorldEvent;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
@SuppressWarnings("WeakerAccess")
@Mod(modid = "@MOD_NAME@", name = "@MOD_NAME@", version = "@MOD_VERSION@", acceptedMinecraftVersions = "@MC_VERSION@")
@NetworkMod(clientSideRequired = false, serverSideRequired = false)
public class TickThreading {
@Mod.Instance
public static TickThreading instance;
private static final int loadedEntityFieldIndex = 0;
private static final int loadedTileEntityFieldIndex = 2;
final Map<World, TickManager> managers = new LinkedHashMap<World, TickManager>();
private final Runtime runtime = Runtime.getRuntime();
private final IntSet worlds = new IntSet();
public String messageDeadlockDetected = "The server appears to have frozen and will restart soon if it does not recover. :(";
public String messageDeadlockRecovered = "The server has recovered and will not need to restart. :)";
public String messageDeadlockSavingExiting = "The server is saving the world and restarting - be right back!";
private String profilingFileName = "world/computer/<computer id>/profile.txt";
public boolean exitOnDeadlock = false;
public boolean requireOpForTicksCommand = true;
public boolean requireOpForProfileCommand = true;
public boolean shouldLoadSpawn = false;
public boolean antiCheatNotify = false;
public boolean cleanWorlds = true;
public boolean allowWorldUnloading = true;
public boolean requireOpForDumpCommand = true;
public boolean enableFastMobSpawning = true;
public boolean rateLimitChunkUpdates = true;
private int saveInterval = 180;
public int deadLockTime = 35;
public int chunkCacheSize = 2000;
public int chunkGCInterval = 1200;
public int maxEntitiesPerPlayer = 1000;
public float mobSpawningMultiplier = 1;
private int tickThreads = 0;
private int profilingInterval = 0;
private int maxItemsPerChunk = 0;
private boolean profilingJson = false;
public boolean variableTickRate = true;
private DeadLockDetector deadLockDetector;
private HashSet<Integer> disabledFastMobSpawningDimensions = new HashSet<Integer>();
private int targetTPS = 20;
private final LeakDetector leakDetector = new LeakDetector(1800);
public static int recentSpawnedItems;
private int lastMaxItemWarnedTime;
static {
new Metrics("TickThreading", VersionUtil.TTVersionNumber());
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
if (!MinecraftServer.getServer().getServerModName().contains("tickthreading")) {
try {
LaunchClassLoader.testForTTChanges();
Log.severe("Patching failed - TT installed correctly as a wrapper, but patches not made.");
} catch (NoSuchMethodError e) {
Log.severe("TT should not be installed in the mods directory. TT is now a server wrapper, put it in the main server directory and launch the TT jar.");
}
System.exit(1);
}
MinecraftForge.EVENT_BUS.register(this);
initPeriodicProfiling();
}
private void initPeriodicProfiling() {
final int profilingInterval = this.profilingInterval;
if (profilingInterval == 0) {
return;
}
TickRegistry.registerScheduledTickHandler(new ProfilingScheduledTickHandler(profilingInterval, MinecraftServer.getServer().getFile(profilingFileName), profilingJson), Side.SERVER);
}
@SuppressWarnings("FieldRepeatedlyAccessedInMethod")
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
Log.fixGuiLogging();
Configuration config = new Configuration(event.getSuggestedConfigurationFile());
config.load();
String GENERAL = Configuration.CATEGORY_GENERAL;
TicksCommand.name = config.get(GENERAL, "ticksCommandName", TicksCommand.name, "Name of the command to be used for performance stats. Defaults to ticks.").getString();
TPSCommand.name = config.get(GENERAL, "tpsCommandName", TPSCommand.name, "Name of the command to be used for TPS reports.").getString();
ProfileCommand.name = config.get(GENERAL, "profileCommandName", ProfileCommand.name, "Name of the command to be used for profiling reports.").getString();
DumpCommand.name = config.get(GENERAL, "dumpCommandName", DumpCommand.name, "Name of the command to be used for profiling reports.").getString();
messageDeadlockDetected = config.get(GENERAL, "messageDeadlockDetected", messageDeadlockDetected, "The message to be displayed if a deadlock is detected. (Only sent if exitOnDeadlock is on)").getString();
messageDeadlockRecovered = config.get(GENERAL, "messageDeadlockRecovered", messageDeadlockRecovered, "The message to be displayed if the server recovers from an apparent deadlock. (Only sent if exitOnDeadlock is on)").getString();
messageDeadlockSavingExiting = config.get(GENERAL, "messageDeadlockSavingExiting", messageDeadlockSavingExiting, "The message to be displayed when the server attempts to save and stop after a deadlock. (Only sent if exitOnDeadlock is on)").getString();
tickThreads = config.get(GENERAL, "tickThreads", tickThreads, "number of threads to use to tick. 0 = automatic").getInt(tickThreads);
saveInterval = config.get(GENERAL, "saveInterval", saveInterval, "Time between partial saves, in ticks.").getInt(saveInterval);
deadLockTime = config.get(GENERAL, "deadLockTime", deadLockTime, "The time(seconds) of being frozen which will trigger the DeadLockDetector. Set to 1 to instead detect lag spikes.").getInt(deadLockTime);
chunkCacheSize = Math.max(100, config.get(GENERAL, "chunkCacheSize", chunkCacheSize, "Number of unloaded chunks to keep cached. Replacement for Forge's dormant chunk cache, which tends to break. Minimum size of 100").getInt(chunkCacheSize));
chunkGCInterval = config.get(GENERAL, "chunkGCInterval", chunkGCInterval, "Interval between chunk garbage collections in ticks").getInt(chunkGCInterval);
targetTPS = config.get(GENERAL, "targetTPS", targetTPS, "TPS the server should try to run at.").getInt(targetTPS);
maxItemsPerChunk = config.get(GENERAL, "maxItemsPerChunk", maxItemsPerChunk, "Maximum number of entity items allowed per chunk. 0 = no limit.").getInt(maxItemsPerChunk);
maxEntitiesPerPlayer = config.get(GENERAL, "maxEntitiesPerPlayer", maxEntitiesPerPlayer, "If more entities than this are loaded per player in a world, mob spawning will be disabled in that world.").getInt(maxEntitiesPerPlayer);
mobSpawningMultiplier = (float) config.get(GENERAL, "mobSpawningMultiplier", mobSpawningMultiplier, "Mob spawning multiplier. Default is 1, can be a decimal.").getDouble(mobSpawningMultiplier);
variableTickRate = config.get(GENERAL, "variableRegionTickRate", variableTickRate, "Allows tick rate to vary per region so that each region uses at most 50ms on average per tick.").getBoolean(variableTickRate);
exitOnDeadlock = config.get(GENERAL, "exitOnDeadlock", exitOnDeadlock, "If the server should shut down when a deadlock is detected").getBoolean(exitOnDeadlock);
enableFastMobSpawning = config.get(GENERAL, "enableFastMobSpawning", enableFastMobSpawning, "If enabled, TT's alternative mob spawning implementation will be used.").getBoolean(enableFastMobSpawning);
requireOpForTicksCommand = config.get(GENERAL, "requireOpsForTicksCommand", requireOpForTicksCommand, "If a player must be opped to use /ticks").getBoolean(requireOpForTicksCommand);
requireOpForProfileCommand = config.get(GENERAL, "requireOpsForProfileCommand", requireOpForProfileCommand, "If a player must be opped to use /profile").getBoolean(requireOpForProfileCommand);
requireOpForDumpCommand = config.get(GENERAL, "requireOpsForDumpCommand", requireOpForDumpCommand, "If a player must be opped to use /dump").getBoolean(requireOpForDumpCommand);
shouldLoadSpawn = config.get(GENERAL, "shouldLoadSpawn", shouldLoadSpawn, "Whether chunks within 200 blocks of world spawn points should always be loaded.").getBoolean(shouldLoadSpawn);
antiCheatNotify = config.get(GENERAL, "antiCheatNotify", antiCheatNotify, "Whether to notify admins if TT anti-cheat detects cheating").getBoolean(antiCheatNotify);
cleanWorlds = config.get(GENERAL, "cleanWorlds", cleanWorlds, "Whether to clean worlds on unload - this should fix some memory leaks due to mods holding on to world objects").getBoolean(cleanWorlds);
allowWorldUnloading = config.get(GENERAL, "allowWorldUnloading", allowWorldUnloading, "Whether worlds should be allowed to unload.").getBoolean(allowWorldUnloading);
profilingInterval = config.get(GENERAL, "profilingInterval", profilingInterval, "Interval, in minutes, to record profiling information to disk. 0 = never. Recommended >= 2.").getInt();
profilingFileName = config.get(GENERAL, "profilingFileName", profilingFileName, "Location to store profiling information to, relative to the server folder. For example, why not store it in a computercraft computer's folder?").getString();
profilingJson = config.get(GENERAL, "profilingJson", profilingJson, "Whether to write periodic profiling in JSON format").getBoolean(profilingJson);
rateLimitChunkUpdates = config.get(GENERAL, "rateLimitChunkUpdates", rateLimitChunkUpdates, "Whether to prevent repeated chunk updates which can cause rendering issues and disconnections for slow clients/connections.").getBoolean(rateLimitChunkUpdates);
config.save();
int[] disabledDimensions = config.get(GENERAL, "disableFastMobSpawningDimensions", new int[]{-1, 1}, "List of dimensions not to enable fast spawning in.").getIntList();
disabledFastMobSpawningDimensions = new HashSet<Integer>(disabledDimensions.length);
for (int disabledDimension : disabledDimensions) {
disabledFastMobSpawningDimensions.add(disabledDimension);
}
PacketCount.allowCounting = false;
}
@Mod.EventHandler
public void serverStarting(FMLServerStartingEvent event) {
if (Loader.isModLoaded("TickProfiler")) {
Log.severe("You're using TickProfiler with TT - TT includes TP's features. Please uninstall TickProfiler, it can cause problems with TT.");
Runtime.getRuntime().exit(1);
}
Log.severe(VersionUtil.versionString() + " is installed on this server."
+ "\nIf anything breaks, check if it is still broken without TickThreading"
+ "\nWe don't want to annoy mod developers with issue reports caused by TickThreading."
+ "\nSeriously, please don't."
+ "\nIf it's only broken with TickThreading, report it at https://github.com/nallar/TickThreading/issues/new"
+ "\n\nAlso, you really should be making regular backups. (You should be doing that even when not using TT.)");
Log.info("Server launch took " + ((System.currentTimeMillis() - LaunchClassLoader.launchTime) / 1000f));
if (Log.debug) {
Log.debug("TickThreading is running in debug mode.");
}
ServerCommandManager serverCommandManager = (ServerCommandManager) event.getServer().getCommandManager();
serverCommandManager.registerCommand(new TicksCommand());
serverCommandManager.registerCommand(new TPSCommand());
serverCommandManager.registerCommand(new ProfileCommand());
serverCommandManager.registerCommand(new DumpCommand());
MinecraftServer.setTargetTPS(targetTPS);
Command.checkForPermissions();
String javaVersion = System.getProperty("java.runtime.version");
if (javaVersion.startsWith("1.6")) {
Log.severe("It is recommended to use a Java 7 JRE. Current version: " + javaVersion);
Log.severe("Java 7 has many performance improvements over 6, and some of TT's changes actually make performance worse when using java 6.");
}
}
@ForgeSubscribe(
priority = EventPriority.HIGHEST
)
public synchronized void onWorldLoad(WorldEvent.Load event) {
World world = event.world;
if (world.isRemote) {
Log.severe("World " + Log.name(world) + " seems to be a client world", new Throwable());
return;
}
if (DimensionManager.getWorld(world.getDimension()) != world) {
Log.severe("World " + world.getName() + " was loaded with an incorrect dimension ID!", new Throwable());
return;
}
if (managers.containsKey(world)) {
Log.severe("World " + world.getName() + "'s world load event was fired twice.", new Throwable());
return;
}
if (!worlds.add(world.provider.dimensionId)) {
Log.severe("World " + world.getName() + " has a duplicate provider dimension ID.\n" + Log.dumpWorlds());
}
world.loadEventFired = true;
TickManager manager = new TickManager((WorldServer) world, getThreadCount());
try {
Field loadedTileEntityField = ReflectUtil.getFields(World.class, List.class)[loadedTileEntityFieldIndex];
new LoadedTileEntityList(world, loadedTileEntityField, manager);
Field loadedEntityField = ReflectUtil.getFields(World.class, List.class)[loadedEntityFieldIndex];
new LoadedEntityList(world, loadedEntityField, manager);
if (managers.put(world, manager) != null) {
Log.severe("World load fired twice for world " + world.getName());
}
} catch (Exception e) {
Log.severe("Failed to initialise threading for world " + Log.name(world), e);
}
if (deadLockDetector == null) {
deadLockDetector = new DeadLockDetector();
}
}
@ForgeSubscribe
public synchronized void onWorldUnload(WorldEvent.Unload event) {
if ((MinecraftServer.getServer().isServerRunning() && !MinecraftServer.getServer().isServerStopped()) && !ContextAccess.$.runningUnder(DimensionManager.class)) {
Log.severe("World unload event fired from unexpected location", new Throwable());
}
World world = event.world;
try {
TickManager tickManager = managers.remove(world);
if (tickManager == null) {
Log.severe("World unload fired twice for world " + world.getName(), new Throwable());
return;
}
tickManager.unload();
Field loadedTileEntityField = ReflectUtil.getFields(World.class, List.class)[loadedTileEntityFieldIndex];
Object loadedTileEntityList = loadedTileEntityField.get(world);
if (!(loadedTileEntityList instanceof EntityList)) {
Log.severe("Looks like another mod broke TT's replacement tile entity list in world: " + Log.name(world));
}
Field loadedEntityField = ReflectUtil.getFields(World.class, List.class)[loadedEntityFieldIndex];
Object loadedEntityList = loadedEntityField.get(world);
if (!(loadedEntityList instanceof EntityList)) {
Log.severe("Looks like another mod broke TT's replacement entity list in world: " + Log.name(world));
}
} catch (Exception e) {
Log.severe("Probable memory leak, failed to unload threading for world " + Log.name(world), e);
}
if (!worlds.remove(world.provider.dimensionId)) {
Log.severe("When removing " + world.getName() + ", its provider dimension ID was not already in the world dimension ID set.\n" + Log.dumpWorlds());
}
if (world instanceof WorldServer) {
((WorldServer) world).stopChunkTickThreads();
leakDetector.scheduleLeakCheck(world, world.getName(), cleanWorlds);
}
}
@ForgeSubscribe
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.action == PlayerInteractEvent.Action.RIGHT_CLICK_BLOCK) {
EntityPlayer entityPlayer = event.entityPlayer;
ItemStack usedItem = entityPlayer.getCurrentEquippedItem();
if (usedItem != null) {
Item usedItemType = usedItem.getItem();
if (usedItemType == Item.pocketSundial && (!requireOpForDumpCommand || entityPlayer.canCommandSenderUseCommand(4, "dump"))) {
Command.sendChat(entityPlayer, DumpCommand.dump(new TableFormatter(entityPlayer), entityPlayer.worldObj, event.x, event.y, event.z, 35).toString());
event.setCanceled(true);
}
}
}
}
public TickManager getManager(World world) {
return managers.get(world);
}
public List<TickManager> getManagers() {
return new ArrayList<TickManager>(managers.values());
}
public boolean shouldFastSpawn(World world) {
return this.enableFastMobSpawning && !disabledFastMobSpawningDimensions.contains(world.getDimension());
}
public int getThreadCount() {
return tickThreads == 0 ? runtime.availableProcessors() + 1 : tickThreads;
}
public boolean removeIfOverMaxItems(final EntityItem e, final Chunk chunk) {
if (maxItemsPerChunk == 0) {
return false;
}
ArrayList<EntityItem> entityItems = chunk.getEntitiesOfType(EntityItem.class);
if (entityItems.size() > maxItemsPerChunk) {
int remaining = 0;
for (EntityItem entityItem : entityItems) {
if (!entityItem.isDead) {
remaining++;
entityItem.combineList(entityItems);
}
}
if (remaining > maxItemsPerChunk) {
e.setDead();
}
if (lastMaxItemWarnedTime < MinecraftServer.currentTick - 10) {
lastMaxItemWarnedTime = MinecraftServer.currentTick;
Log.warning("Entity items in chunk " + chunk + " exceeded limit of " + maxItemsPerChunk);
}
return e.isDead;
}
return false;
}
public static boolean checkSaveInterval(int tickCount) {
int saveInterval = instance.saveInterval;
return saveInterval > 0 && tickCount % saveInterval == 0;
}
private static class ProfilingScheduledTickHandler implements IScheduledTickHandler {
private static final EnumSet<TickType> TICKS = EnumSet.of(TickType.SERVER);
private final int profilingInterval;
private final File profilingFile;
private final boolean json;
ProfilingScheduledTickHandler(final int profilingInterval, final File profilingFile, final boolean json) {
this.profilingInterval = profilingInterval;
this.profilingFile = profilingFile;
this.json = json;
}
@Override
public int nextTickSpacing() {
return profilingInterval * 60 * 20;
}
@Override
public void tickStart(final EnumSet<TickType> type, final Object... tickData) {
final EntityTickProfiler entityTickProfiler = EntityTickProfiler.ENTITY_TICK_PROFILER;
entityTickProfiler.startProfiling(new Runnable() {
@Override
public void run() {
try {
TableFormatter tf = new TableFormatter(MinecraftServer.getServer());
tf.tableSeparator = "\n";
if (json) {
entityTickProfiler.writeJSONData(profilingFile);
} else {
Files.write(entityTickProfiler.writeStringData(tf, 6).toString(), profilingFile, Charsets.UTF_8);
}
} catch (Throwable t) {
Log.severe("Failed to save periodic profiling data to " + profilingFile, t);
}
}
}, ProfileCommand.ProfilingState.GLOBAL, 10, Arrays.<World>asList(DimensionManager.getWorlds()));
}
@Override
public void tickEnd(final EnumSet<TickType> type, final Object... tickData) {
}
@Override
public EnumSet<TickType> ticks() {
return TICKS;
}
@Override
public String getLabel() {
return "TickThreading scheduled profiling handler";
}
}
}