diff --git a/Server/src/main/java/net/simon987/server/GameServer.java b/Server/src/main/java/net/simon987/server/GameServer.java index b7da025..698346f 100644 --- a/Server/src/main/java/net/simon987/server/GameServer.java +++ b/Server/src/main/java/net/simon987/server/GameServer.java @@ -1,197 +1,214 @@ -package net.simon987.server; - - -import net.simon987.server.event.GameEvent; -import net.simon987.server.event.GameEventDispatcher; -import net.simon987.server.event.TickEvent; -import net.simon987.server.game.GameUniverse; -import net.simon987.server.game.World; -import net.simon987.server.logging.LogManager; -import net.simon987.server.plugin.PluginManager; -import net.simon987.server.plugin.ServerPlugin; -import net.simon987.server.user.User; -import net.simon987.server.webserver.SocketServer; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; - -public class GameServer implements Runnable { - - public final static GameServer INSTANCE = new GameServer(); - - private GameUniverse gameUniverse; - private GameEventDispatcher eventDispatcher; - private PluginManager pluginManager; - - private ServerConfiguration config; - - private SocketServer socketServer; - - private int maxExecutionTime; - - public GameServer() { - - this.config = new ServerConfiguration(new File("config.properties")); - - gameUniverse = new GameUniverse(config); - pluginManager = new PluginManager(); - - maxExecutionTime = config.getInt("user_timeout"); - - //Load all plugins in plugins folder, if it doesn't exist, create it - File pluginDir = new File("plugins/"); - File[] pluginDirListing = pluginDir.listFiles(); - - if (pluginDirListing != null) { - for (File pluginFile : pluginDirListing) { - - if (pluginFile.getName().endsWith(".jar")) { - pluginManager.load(pluginFile); - } - - } - } else { - if (!pluginDir.mkdir()) { - LogManager.LOGGER.severe("Couldn't create plugin directory"); - } - } - - eventDispatcher = new GameEventDispatcher(pluginManager); - - } - - public GameUniverse getGameUniverse() { - return gameUniverse; - } - - public GameEventDispatcher getEventDispatcher() { - return eventDispatcher; - } - - @Override - public void run() { - LogManager.LOGGER.info("(G) Started game loop"); - - long startTime; //Start time of the loop - long uTime; //update time - long waitTime; //time to wait - - boolean running = true; - - while (running) { - - startTime = System.currentTimeMillis(); - - tick(); - - uTime = System.currentTimeMillis() - startTime; - waitTime = config.getInt("tick_length") - uTime; - - LogManager.LOGGER.info("Wait time : " + waitTime + "ms | Update time: " + uTime + "ms | " + (int) (((double) uTime / waitTime) * 100) + "% load"); - - try { - if (waitTime >= 0) { - Thread.sleep(waitTime); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - - } - - - } - - private void tick() { - gameUniverse.incrementTime(); - - //Dispatch tick event - GameEvent event = new TickEvent(gameUniverse.getTime()); - GameServer.INSTANCE.getEventDispatcher().dispatch(event); //Ignore cancellation - - - //Process user code - ArrayList users_ = gameUniverse.getUsers(); - for (User user : users_) { - - if (user.getCpu() != null) { - try { - - int timeout = Math.min(user.getControlledUnit().getEnergy(), maxExecutionTime); - - user.getCpu().reset(); - int cost = user.getCpu().execute(timeout); - user.getControlledUnit().spendEnergy(cost); - - } catch (Exception e) { - LogManager.LOGGER.severe("Error executing " + user.getUsername() + "'s code"); - e.printStackTrace(); - } - - } - } - - //Process each worlds - //Avoid concurrent modification - ArrayList worlds = new ArrayList<>(gameUniverse.getWorlds()); - for (World world : worlds) { - world.update(); - } - - //Save - if (gameUniverse.getTime() % config.getInt("save_interval") == 0) { - save(new File("save.json")); - } - - socketServer.tick(); - - LogManager.LOGGER.info("Processed " + gameUniverse.getWorlds().size() + " worlds"); - } - - /** - * Save game universe to file in JSON format - * - * @param file JSON file to save - */ - public void save(File file) { - - try { - FileWriter fileWriter = new FileWriter(file); - - JSONObject universe = gameUniverse.serialise(); - - JSONArray plugins = new JSONArray(); - - for (ServerPlugin plugin : pluginManager.getPlugins()) { - plugins.add(plugin.serialise()); - } - - universe.put("plugins", plugins); - - fileWriter.write(universe.toJSONString()); - fileWriter.close(); - - LogManager.LOGGER.info("Saved to file " + file.getName()); - - } catch (IOException e) { - e.printStackTrace(); - } - - } - - public ServerConfiguration getConfig() { - return config; - } - - public PluginManager getPluginManager() { - return pluginManager; - } - - public void setSocketServer(SocketServer socketServer) { - this.socketServer = socketServer; - } -} +package net.simon987.server; + +import net.simon987.server.event.GameEvent; +import net.simon987.server.event.GameEventDispatcher; +import net.simon987.server.event.TickEvent; +import net.simon987.server.game.GameUniverse; +import net.simon987.server.game.World; +import net.simon987.server.logging.LogManager; +import net.simon987.server.plugin.PluginManager; +import net.simon987.server.plugin.ServerPlugin; +import net.simon987.server.user.User; +import net.simon987.server.webserver.SocketServer; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +public class GameServer implements Runnable { + + public final static GameServer INSTANCE = new GameServer(); + + private GameUniverse gameUniverse; + private GameEventDispatcher eventDispatcher; + private PluginManager pluginManager; + + private ServerConfiguration config; + + private SocketServer socketServer; + + private int maxExecutionTime; + + public ArrayList saveArchive; + + public int maxArchiveSize; + + public GameServer() { + + this.config = new ServerConfiguration(new File("config.properties")); + + gameUniverse = new GameUniverse(config); + pluginManager = new PluginManager(); + + maxExecutionTime = config.getInt("user_timeout"); + + // Load all plugins in plugins folder, if it doesn't exist, create it + File pluginDir = new File("plugins/"); + File[] pluginDirListing = pluginDir.listFiles(); + + if (pluginDirListing != null) { + for (File pluginFile : pluginDirListing) { + + if (pluginFile.getName().endsWith(".jar")) { + pluginManager.load(pluginFile); + } + + } + } else { + if (!pluginDir.mkdir()) { + LogManager.LOGGER.severe("Couldn't create plugin directory"); + } + } + + eventDispatcher = new GameEventDispatcher(pluginManager); + + saveArchive = new ArrayList(); + + maxArchiveSize = config.getInt("max_archive_size"); + } + + public GameUniverse getGameUniverse() { + return gameUniverse; + } + + public GameEventDispatcher getEventDispatcher() { + return eventDispatcher; + } + + @Override + public void run() { + LogManager.LOGGER.info("(G) Started game loop"); + + long startTime; // Start time of the loop + long uTime; // update time + long waitTime; // time to wait + + boolean running = true; + + while (running) { + + startTime = System.currentTimeMillis(); + + tick(); + + uTime = System.currentTimeMillis() - startTime; + waitTime = config.getInt("tick_length") - uTime; + + LogManager.LOGGER.info("Wait time : " + waitTime + "ms | Update time: " + uTime + "ms | " + + (int) (((double) uTime / waitTime) * 100) + "% load"); + + try { + if (waitTime >= 0) { + Thread.sleep(waitTime); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + + } + + } + + private void tick() { + gameUniverse.incrementTime(); + + // Dispatch tick event + GameEvent event = new TickEvent(gameUniverse.getTime()); + GameServer.INSTANCE.getEventDispatcher().dispatch(event); // Ignore cancellation + + // Process user code + ArrayList users_ = gameUniverse.getUsers(); + for (User user : users_) { + + if (user.getCpu() != null) { + try { + + int timeout = Math.min(user.getControlledUnit().getEnergy(), maxExecutionTime); + + user.getCpu().reset(); + int cost = user.getCpu().execute(timeout); + user.getControlledUnit().spendEnergy(cost); + + } catch (Exception e) { + LogManager.LOGGER.severe("Error executing " + user.getUsername() + "'s code"); + e.printStackTrace(); + } + + } + } + + // Process each worlds + // Avoid concurrent modification + ArrayList worlds = new ArrayList<>(gameUniverse.getWorlds()); + for (World world : worlds) { + world.update(); + } + + // Save + if (gameUniverse.getTime() % config.getInt("save_interval") == 0) { + save(new File("save.json")); + } + + socketServer.tick(); + + LogManager.LOGGER.info("Processed " + gameUniverse.getWorlds().size() + " worlds"); + } + + /** + * Save game universe to file in JSON format + * + * @param file + * JSON file to save + */ + public void save(File file) { + + if (new File(new File("save.json").getAbsolutePath()).exists()) { + saveArchive.add(ZipUtils.bytifyFile("save.json")); + while(saveArchive.size() > maxArchiveSize) { + saveArchive.remove(0); + } + } + + try { + FileWriter fileWriter = new FileWriter(file); + + JSONObject universe = gameUniverse.serialise(); + + JSONArray plugins = new JSONArray(); + + for (ServerPlugin plugin : pluginManager.getPlugins()) { + plugins.add(plugin.serialise()); + } + + universe.put("plugins", plugins); + + fileWriter.write(universe.toJSONString()); + fileWriter.close(); + + LogManager.LOGGER.info("Saved to file " + file.getName()); + + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public ServerConfiguration getConfig() { + return config; + } + + public PluginManager getPluginManager() { + return pluginManager; + } + + public void setSocketServer(SocketServer socketServer) { + this.socketServer = socketServer; + } + + public ArrayList getSaveArchive() { + return this.saveArchive; + } +} diff --git a/Server/src/main/java/net/simon987/server/Main.java b/Server/src/main/java/net/simon987/server/Main.java index 5875215..6821f32 100644 --- a/Server/src/main/java/net/simon987/server/Main.java +++ b/Server/src/main/java/net/simon987/server/Main.java @@ -1,29 +1,41 @@ -package net.simon987.server; - -import net.simon987.server.logging.LogManager; -import net.simon987.server.webserver.SocketServer; - -import java.io.File; -import java.net.InetSocketAddress; - - -public class Main { - public static void main(String[] args) { - - - LogManager.initialize(); - ServerConfiguration config = new ServerConfiguration(new File("config.properties")); - - //Load - GameServer.INSTANCE.getGameUniverse().load(new File("save.json")); - - SocketServer socketServer = new SocketServer(new InetSocketAddress(config.getString("webSocket_host"), - config.getInt("webSocket_port")), config); - - GameServer.INSTANCE.setSocketServer(socketServer); - - - (new Thread(socketServer)).start(); - (new Thread(GameServer.INSTANCE)).start(); - } -} +package net.simon987.server; + +import net.simon987.server.logging.LogManager; +import net.simon987.server.webserver.SocketServer; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; + + +public class Main { + public static void main(String[] args) { + + //Writes all of the files stored in GameServer.saveArray to a zip file. + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + public void run() { + try { + ZipUtils.writeSavesToZip(GameServer.INSTANCE.getSaveArchive()); + } catch (IOException e) { + System.out.println("Error writing saves to zip"); + e.printStackTrace(); + } + } + }, "Shutdown-thread")); + + LogManager.initialize(); + ServerConfiguration config = new ServerConfiguration(new File("config.properties")); + + //Load + GameServer.INSTANCE.getGameUniverse().load(new File("save.json")); + + SocketServer socketServer = new SocketServer(new InetSocketAddress(config.getString("webSocket_host"), + config.getInt("webSocket_port")), config); + + GameServer.INSTANCE.setSocketServer(socketServer); + + + (new Thread(socketServer)).start(); + (new Thread(GameServer.INSTANCE)).start(); + } +} diff --git a/Server/src/main/java/net/simon987/server/ZipUtils.java b/Server/src/main/java/net/simon987/server/ZipUtils.java new file mode 100644 index 0000000..906219e --- /dev/null +++ b/Server/src/main/java/net/simon987/server/ZipUtils.java @@ -0,0 +1,91 @@ +package net.simon987.server; + +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import net.simon987.server.logging.LogManager; + +public class ZipUtils { + + private static final int BUFFER_SIZE = 1024; + + public static byte[] bytifyFile(String fileName) { + + Path path = Paths.get(fileName); + byte[] bytes = null; + + try { + bytes = Files.readAllBytes(path); + + } catch (IOException e) { + System.out.println("Failed to extract bytes from: " + fileName); + e.printStackTrace(); + } + + return bytes; + } + + public static String getByteArrAsString(byte[] bytes) throws UnsupportedEncodingException { + return new String(bytes, "UTF-8"); + } + + public static void writeSavesToZip(ArrayList array) throws IOException { + + int writeCount = 0; + FileOutputStream output = new FileOutputStream("archive_" + getDateTimeStamp() + ".zip"); + ZipOutputStream stream = new ZipOutputStream(output); + byte[] buffer = new byte[BUFFER_SIZE]; + ByteArrayInputStream bais = new ByteArrayInputStream(buffer); + + while ((bais.read(buffer)) > -1) { + for (int i = 0; i < array.size(); i++) { + + ZipEntry entry = new ZipEntry("save_" + getTickTime(array.get(i)) + ".json"); + stream.putNextEntry(entry); + stream.write(array.get(i)); + stream.closeEntry(); + writeCount++; + } + } + + stream.close(); + output.close(); + + LogManager.LOGGER.info(writeCount + " saves moved to zip file archive"); + } + + private static String getTickTime(byte[] bytes) throws UnsupportedEncodingException { + + Pattern pattern = Pattern.compile("\"time\""); + String stringedBytes = getByteArrAsString(bytes); + Matcher matcher = pattern.matcher(stringedBytes); + int startIndex = 0; + + while (matcher.find()) { + startIndex = matcher.end() + 1; + } + + int endIndex = stringedBytes.indexOf(",", startIndex); + + return stringedBytes.substring(startIndex, endIndex); + } + + private static String getDateTimeStamp() { + Date millisToDate = new Date(System.currentTimeMillis()); + SimpleDateFormat f = new SimpleDateFormat("yyyyMMddHHmmss"); + return f.format(millisToDate); + } + +} diff --git a/config.properties b/config.properties index 65e87bc..0764880 100644 --- a/config.properties +++ b/config.properties @@ -1,65 +1,68 @@ -# MySQL username -mysql_user=mar -# MySQL password/ -mysql_pass=mar -# MySQL address -mysql_url=jdbc:mysql://localhost:3306/mar?useSSL=false -save_interval=10 -# Web server port -webSocket_port=8887 -webSocket_host=0.0.0.0 - -use_secure_webSocket=0 -cert_path=certificates -# ---------------------------------------------- - -# Length of a tick in ms -tick_length=1000 -# Default offset of the origin (starting point of code execution) in words -org_offset=1024 -# Address of the stack bottom -stack_bottom=32768 -# Size of the memory in bytes -memory_size=65536 -# Initial location of new user's controlled unit -new_user_worldX = 32767 -new_user_worldY = 32767 -# Default user code -new_user_code=; Welcome to Much Assembly required!\n\ - ; You will find useful information on the game here: https://github.com/simon987/Much-Assembly-Required/wiki\n\ - .text\n\ - \t; Write code here\n\ - \tbrk -# Default held item -new_user_item=0 -# ---------------------------------------------- -# Biomass units yield for a plant -plant_yield=2 -# Grow time in ticks for a plant to grow -plant_grow_time=32 -# Minimum tree count for the WorldGenerator -minTreeCount=3 -# Maximum tree count for the WorldGenerator -maxTreeCount=10 -# Maximum energy of the battery hardware in kJ -battery_max_energy=60000 -# ---------------------------------------------- -# Minimum center point count for the WorldGenerator -wg_centerPointCountMin=5 -# Maximum center point count for the WorldGenerator -wg_centerPointCountMax=15 -# Wall/Plain tile ratio for the WorldGenerator -wg_wallPlainRatio=4 -# Minimum iron tiles count for the WorldGenerator -wg_minIronCount=0 -# Minimum iron tile count for the WorldGenerator -wg_maxIronCount=2 -# Minimum copper tile count for the WorldGenerator -wg_minCopperCount=0 -# Maximum copper tile count for the WorldGenerator -wg_maxCopperCount=2 -# ---------------------------------------------- -# Maximum execution time of user code in ms -user_timeout=500 -# Free CPU execution time in ms -user_free_execution_time=2 \ No newline at end of file +# MySQL username +mysql_user=mar +# MySQL password/ +mysql_pass=mar +# MySQL address +mysql_url=jdbc:mysql://localhost:3306/mar?useSSL=false +save_interval=10 +# Web server port +webSocket_port=8887 +webSocket_host=0.0.0.0 + +use_secure_webSocket=0 +cert_path=certificates +# ---------------------------------------------- + +# Length of a tick in ms +tick_length=1000 +# Default offset of the origin (starting point of code execution) in words +org_offset=1024 +# Address of the stack bottom +stack_bottom=32768 +# Size of the memory in bytes +memory_size=65536 +# Initial location of new user's controlled unit +new_user_worldX = 32767 +new_user_worldY = 32767 +# Default user code +new_user_code=; Welcome to Much Assembly required!\n\ + ; You will find useful information on the game here: https://github.com/simon987/Much-Assembly-Required/wiki\n\ + .text\n\ + \t; Write code here\n\ + \tbrk +# Default held item +new_user_item=0 +# ---------------------------------------------- +# Biomass units yield for a plant +plant_yield=2 +# Grow time in ticks for a plant to grow +plant_grow_time=32 +# Minimum tree count for the WorldGenerator +minTreeCount=3 +# Maximum tree count for the WorldGenerator +maxTreeCount=10 +# Maximum energy of the battery hardware in kJ +battery_max_energy=60000 +# ---------------------------------------------- +# Minimum center point count for the WorldGenerator +wg_centerPointCountMin=5 +# Maximum center point count for the WorldGenerator +wg_centerPointCountMax=15 +# Wall/Plain tile ratio for the WorldGenerator +wg_wallPlainRatio=4 +# Minimum iron tiles count for the WorldGenerator +wg_minIronCount=0 +# Minimum iron tile count for the WorldGenerator +wg_maxIronCount=2 +# Minimum copper tile count for the WorldGenerator +wg_minCopperCount=0 +# Maximum copper tile count for the WorldGenerator +wg_maxCopperCount=2 +# ---------------------------------------------- +# Maximum execution time of user code in ms +user_timeout=500 +# Free CPU execution time in ms +user_free_execution_time=2 +# ---------------------------------------------- +# Max saves to archive when the server is shutdown +max_archive_size=10 \ No newline at end of file