Java tutorial
/* * Enderstone * Copyright (C) 2014 Sander Gielisse and Fernando van Loenhout * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.enderstone.server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import javax.imageio.ImageIO; import javax.xml.bind.DatatypeConverter; import org.enderstone.server.api.event.Cancellable; import org.enderstone.server.api.event.Event; import org.enderstone.server.api.messages.Message; import org.enderstone.server.commands.CommandMap; import org.enderstone.server.commands.enderstone.AiCommand; import org.enderstone.server.commands.enderstone.CraftingDebugCommand; import org.enderstone.server.commands.enderstone.DebugCommand; import org.enderstone.server.commands.enderstone.LagCommand; import org.enderstone.server.commands.enderstone.PingCommand; import org.enderstone.server.commands.enderstone.QuitCommand; import org.enderstone.server.commands.enderstone.TickRateCommand; import org.enderstone.server.commands.enderstone.VersionCommand; import org.enderstone.server.commands.enderstone.WorldCommand; import org.enderstone.server.commands.vanilla.GameModeCommand; import org.enderstone.server.commands.vanilla.KillCommand; import org.enderstone.server.commands.vanilla.StopCommand; import org.enderstone.server.commands.vanilla.TeleportCommand; import org.enderstone.server.commands.vanilla.TellCommand; import org.enderstone.server.entity.EnderEntity; import org.enderstone.server.entity.player.EnderPlayer; import org.enderstone.server.inventory.DefaultCraftingRecipes; import org.enderstone.server.packet.ConnectionInitializer; import org.enderstone.server.packet.Packet; import org.enderstone.server.packet.play.PacketKeepAlive; import org.enderstone.server.packet.play.PacketOutChatMessage; import org.enderstone.server.packet.play.PacketOutUpdateTime; import org.enderstone.server.regions.EnderWorld; import org.enderstone.server.regions.generators.FlyingIslandsGenerator; import org.enderstone.server.regions.generators.SimpleGenerator; import org.enderstone.server.util.NettyThreadFactory; import org.enderstone.server.uuid.UUIDFactory; public class Main implements Runnable { public static final String NAME = "Enderstone"; public static final String VERSION = "1.0.0"; public static final String PROTOCOL_VERSION = "1.8"; /** * Amount of server ticks in 1 second */ private int tickSpeed = 20; /** * time between ticks in ms */ private int tickTime = 1000 / tickSpeed; public static final int CANT_KEEP_UP_TIMEOUT = -10000; public static final int MAX_VIEW_DISTANCE = 10; public static final int MAX_NETTY_BOSS_THREADS = 4; public static final int MAX_NETTY_WORKER_THREADS = 8; public static final int MAX_SLEEP = 100; public static final int DEFAULT_PROTOCOL = 47; public static final Set<Integer> SUPPORTED_PROTOCOLS = Collections.unmodifiableSet(new HashSet<Integer>() { private static final long serialVersionUID = 1L; { this.add(47); // 1.8 this.add(107); // 1.9 this.add(108); // 1.9.1 this.add(109); // 1.9.2 this.add(110); // 1.9.3 - 1.9.4 this.add(210); // 1.10 this.add(315); // 1.11 this.add(316); // 1.11.2 // first four 1.12 snapshots, Why not. this.add(317); this.add(318); this.add(319); this.add(320); } }); public static final String[] AUTHORS = new String[] { "Sander Gielisse [sander2798]", "Fernando van Loenhout [ferrybig]" }; public static final String[] TOP_CONTRIBUTORS = new String[] { "Gyroninja" }; public static final Random random = new Random(); public volatile Thread mainThread; public final List<Thread> listenThreads = new CopyOnWriteArrayList<>(); public boolean onlineMode = false; public Properties prop = null; public volatile String motd; public volatile int maxPlayers = 20; public UUIDFactory uuidFactory = new UUIDFactory(); public String FAVICON = null; public int port; public boolean doPhysics = true; public volatile boolean isRunning = true; private long tick = 0; public final CommandMap commands; { commands = new CommandMap(); commands.registerCommand(new CraftingDebugCommand()); commands.registerCommand(new DebugCommand()); commands.registerCommand(new GameModeCommand()); commands.registerCommand(new KillCommand()); commands.registerCommand(new LagCommand()); commands.registerCommand(new PingCommand()); commands.registerCommand(new QuitCommand()); commands.registerCommand(new StopCommand()); commands.registerCommand(new TeleportCommand()); commands.registerCommand(new TellCommand()); commands.registerCommand(new VersionCommand()); commands.registerCommand(new WorldCommand()); commands.registerCommand(new CraftingDebugCommand()); commands.registerCommand(new LagCommand()); commands.registerCommand(new AiCommand()); commands.registerCommand(new TickRateCommand()); } private static Main instance; /** * This array is used to store the last lag of the server, it can be used by plugins to calculate the lag */ private final long[] lastTickSlices = new long[128]; private int lastTickPointer = 0; public volatile int playerCount = 0; // high performance solution for getting the onlinePlayers.size() from an async thread public final Set<EnderPlayer> onlinePlayers = new HashSet<>(); public final List<EnderWorld> worlds = new ArrayList<>(); private final List<Runnable> sendToMainThread = new ArrayList<Runnable>(); // don't forget to synchronize public static Main getInstance() { return instance; } public void sendToMainThread(Runnable run) { if (isCurrentThreadMainThread()) run.run(); else synchronized (sendToMainThread) { sendToMainThread.add(run); } } public static void main(String[] args) { new Main().run(); } @Override public void run() { Main.instance = this; EnderLogger.info("Starting " + NAME + " " + VERSION + " server version " + PROTOCOL_VERSION + "."); EnderLogger.info("Authors: " + Arrays.asList(AUTHORS).toString()); EnderLogger .info("Top contributors: " + Arrays.asList(TOP_CONTRIBUTORS).toString() + " <--- Thanks to them!"); EnderLogger.info("Loading server.properties file..."); this.loadConfigFromDisk(); this.motd = (String) prop.get("motd"); // TODO read max players from config // this.maxPlayers = ; EnderLogger.info("Loaded server.properties file!"); EnderLogger.info("Loading favicon..."); try { if (readFavicon()) EnderLogger.info("Loaded server-icon.png!"); } catch (FileNotFoundException e) { EnderLogger.info("server-icon.png not found!"); } catch (IOException e) { EnderLogger.warn("Error while reading server-icon.png!"); EnderLogger.exception(e); } EnderLogger.info("Server ready... Starting required threads now!"); final ThreadGroup nettyListeners = new ThreadGroup(Thread.currentThread().getThreadGroup(), "Netty Listeners"); for (final int nettyPort : new int[] { this.port }) { Thread t; (t = new Thread(nettyListeners, new Runnable() { @Override public void run() { EnderLogger.info("Started Netty Server at port " + nettyPort + "..."); ThreadGroup group = new ThreadGroup(nettyListeners, "Listener-" + nettyPort); EventLoopGroup bossGroup = new NioEventLoopGroup(MAX_NETTY_BOSS_THREADS, new NettyThreadFactory(group, "boss")); EventLoopGroup workerGroup = new NioEventLoopGroup(MAX_NETTY_WORKER_THREADS, new NettyThreadFactory(group, "worker")); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup); bootstrap.channel(NioServerSocketChannel.class); bootstrap.childHandler(new ConnectionInitializer()); bootstrap.bind(nettyPort).sync().channel().closeFuture().sync(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { EnderLogger.info("Stopped Netty Server at port " + nettyPort + "..."); scheduleShutdown(); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }, "Listener-" + nettyPort)).start(); this.listenThreads.add(t); } (mainThread = new Thread(new Runnable() { long lastTick = System.currentTimeMillis(); @Override public void run() { EnderLogger.info("Main Server Thread initialized and started!"); EnderLogger.info("" + NAME + " Server started, " + PROTOCOL_VERSION + " clients can now connect to port " + port + "!"); // TODO support multiple worlds with a simple and good working system worlds.add(new EnderWorld("world1", new SimpleGenerator(), new File("world1"))); worlds.add(new EnderWorld("world2", new FlyingIslandsGenerator(), new File("world2"))); try { while (Main.this.isRunning) { mainServerTick(); } } catch (InterruptedException e) { Main.this.isRunning = false; Thread.currentThread().interrupt(); } catch (RuntimeException ex) { EnderLogger.error("CRASH REPORT! (this should not happen!)"); EnderLogger.error("Main thread has shut down, this shouldn't happen!"); EnderLogger.exception(ex); EnderLogger.error("Server was processing tick " + tick); EnderLogger.error("Last succesfull tick was " + new Date(lastTick).toString()); } finally { Main.this.isRunning = false; Main.getInstance().directShutdown(); EnderLogger.info("Main Server Thread stopped!"); } } private void mainServerTick() throws InterruptedException { synchronized (sendToMainThread) { for (Runnable run : sendToMainThread) { try { run.run(); } catch (Exception e) { EnderLogger.warn("Problem while executing task " + run.toString()); EnderLogger.exception(e); } } sendToMainThread.clear(); } try { serverTick(tick); } catch (Exception e) { EnderLogger.error("Problem while running ServerTick()"); EnderLogger.exception(e); } this.lastTick += getTickTime(); long sleepTime = (lastTick) - System.currentTimeMillis(); Main.this.lastTickSlices[Main.this.lastTickPointer] = sleepTime; if (++Main.this.lastTickPointer >= Main.this.lastTickSlices.length) Main.this.lastTickPointer = 0; if (sleepTime < Main.CANT_KEEP_UP_TIMEOUT) { this.warn("Can't keep up! " + -(sleepTime / tickTime) + " ticks behind!"); this.lastTick = System.currentTimeMillis(); } else if (sleepTime > Main.MAX_SLEEP) { this.warn("Did the system time change?"); this.lastTick = System.currentTimeMillis(); } else if (sleepTime > 0) { Thread.sleep(sleepTime); } else { if (Thread.interrupted()) { throw new InterruptedException(); } } tick++; } public void warn(String warn) { EnderLogger.warn("[ServerThread] [tick-" + tick + "] " + warn); } }, "ServerThread")).start(); ThreadGroup shutdownHooks = new ThreadGroup(Thread.currentThread().getThreadGroup(), "Shutdown hooks"); Runtime.getRuntime().addShutdownHook(new Thread(shutdownHooks, new Runnable() { @Override public void run() { Main.this.scheduleShutdown(); boolean interrupted = false; boolean joined = false; do { try { mainThread.join(); joined = true; } catch (InterruptedException ex) { interrupted = true; } } while (!joined); if (interrupted) Thread.currentThread().interrupt(); } }, "Server stopping")); } private boolean readFavicon() throws IOException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { BufferedImage image = ImageIO.read(new File("server-icon.png")); if (image.getWidth() == 64 && image.getHeight() == 64) { ImageIO.write(image, "png", baos); baos.flush(); FAVICON = "data:image/png;base64," + DatatypeConverter.printBase64Binary(baos.toByteArray()); return true; } else { EnderLogger.warn("Your server-icon.png needs to be 64*64!"); return false; } } catch (Exception e) { return false; } } /** * Gets the current servertick. may overflow to below zero * if the server is running longer than the length of the universe * @return */ public long getCurrentServerTick() { return this.tick; } public void saveConfigToDisk(boolean defaultt) { try (OutputStream output = new FileOutputStream("server.properties")) { if (defaultt) { prop.setProperty("motd", "Another Enderstone server!"); prop.setProperty("port", "25565"); prop.setProperty("max-players", "20"); prop.setProperty("view-distance", "7"); } prop.store(output, "Enderstone Server Config!"); } catch (IOException e1) { EnderLogger.exception(e1); } } public Properties loadConfigFromDisk() { prop = new Properties(); try (InputStream input = new FileInputStream("server.properties")) { prop.load(input); port = Integer.parseInt(prop.getProperty("port")); } catch (FileNotFoundException e) { e.printStackTrace(); this.saveConfigToDisk(true); this.loadConfigFromDisk(); } catch (IOException e) { EnderLogger.exception(e); } return prop; } /** * Get a player by its username * @param name * @return */ public EnderPlayer getPlayer(String name) { for (EnderPlayer ep : this.onlinePlayers) { if (ep.getPlayerName().equals(name)) { return ep; } } return null; } /** * Gets a player by its UUID */ public EnderPlayer getPlayer(UUID uuid) { for (EnderPlayer ep : this.onlinePlayers) { if (ep.uuid.equals(uuid)) { return ep; } } return null; } private long latestKeepAlive = 0; private long latestChunkUpdate = 0; private void serverTick(long tick) { int recepies = DefaultCraftingRecipes.serverTick(); if (recepies != -1) { EnderLogger.info(recepies + " crafting recipes listeners loaded!"); } this.playerCount = this.onlinePlayers.size(); boolean doKeepAliveUpdate = (latestKeepAlive++ & 0b0011_1111) == 0; // faster than % 64 == 0 boolean doChunkUpdate = (latestChunkUpdate++ & 0b0001_1111) == 0; // faster than % 31 == 0 boolean doUpdateTimeAndWeather = (tick & 0b0011_1111) == 0; // faster than % 64 == 0 for (EnderPlayer p : onlinePlayers) { p.serverTick(); if (doKeepAliveUpdate) { p.getNetworkManager() .sendPacket(new PacketKeepAlive(p.keepAliveID = random.nextInt(Integer.MAX_VALUE))); } if (doChunkUpdate && !p.isDead()) { p.getWorld().doChunkUpdatesForPlayer(p, p.chunkInformer, Math.min(p.clientSettings.renderDistance - 1, MAX_VIEW_DISTANCE)); p.updatePlayers(onlinePlayers); } for (EnderWorld world : worlds) { world.updateEntities(onlinePlayers); } if (doUpdateTimeAndWeather) { p.getNetworkManager().sendPacket(new PacketOutUpdateTime(tick, this.getWorld(p).getTime())); } } for (EnderWorld world : worlds) { world.serverTick(); } } public void broadcastMessage(Message message) { EnderLogger.info(message.toPlainText()); Packet p = new PacketOutChatMessage(message, (byte) 1); for (EnderPlayer player : Main.getInstance().onlinePlayers) { player.getNetworkManager().sendPacket(p); } } /** * Schedule a server shutdown, calling this methods tells the main thread that the server needs to shutdown */ public void scheduleShutdown() { this.mainThread.interrupt(); isRunning = false; } /** * Any mainthread-shutdown logic belongs to this method */ private void directShutdown() { if (this.mainThread != null) { this.mainThread.interrupt(); } for (Thread t : this.listenThreads) { t.interrupt(); } boolean interrupted = false; for (Thread t : this.listenThreads) { boolean joined = false; do { try { t.join(); joined = true; } catch (InterruptedException ex) { interrupted = true; } } while (!joined); } if (interrupted) Thread.currentThread().interrupt(); } public EnderPlayer getPlayer(int entityId) { for (EnderPlayer ep : this.onlinePlayers) { if (ep.getEntityId() == entityId) { return ep; } } return null; } public static boolean isCurrentThreadMainThread() { return Main.getInstance().mainThread == Thread.currentThread(); } public EnderWorld getWorld(EnderPlayer player) { for (EnderWorld world : this.worlds) { if (world.players.contains(player)) { return world; } } return null; } public EnderEntity getEntityById(int targetId) { for (EnderWorld w : this.worlds) { for (EnderEntity e : w.entities) { if (e.getEntityId() == targetId) { return e; } } } for (EnderPlayer ep : this.onlinePlayers) { if (ep.getEntityId() == targetId) { return ep; } } return null; } public boolean callEvent(Event e) { // TODO call events if (e instanceof Cancellable) { return ((Cancellable) e).isCancelled(); } return false; } public long[] getLastLag() { long[] last = new long[this.lastTickSlices.length]; if (this.lastTickPointer == 0) System.arraycopy(this.lastTickSlices, 0, last, 0, last.length); else { System.arraycopy(this.lastTickSlices, this.lastTickPointer, last, 0, last.length - this.lastTickPointer); System.arraycopy(this.lastTickSlices, 0, last, last.length - this.lastTickPointer, this.lastTickPointer); } return last; } /** * @return the tickSpeed */ public int getTickSpeed() { return tickSpeed; } /** * @param tickSpeed the tickSpeed to set */ public void setTickSpeed(int tickSpeed) { if (tickSpeed <= 0) throw new IllegalArgumentException("tickSpeed <= 0: " + tickSpeed); this.tickSpeed = tickSpeed; this.tickTime = 1000 / tickSpeed; } /** * @return the tickTime */ public int getTickTime() { return tickTime; } }