Added functionality to archive list of saves when the game server exits.

The number of saves that will be archived can be modified using
max_archive_size in config.properties. Added shutdown hook to Main.java
to handle dumping of zip files. Essential functions are in
ZipUtils.java.
This commit is contained in:
Steven Berdak 2017-12-01 12:04:06 -08:00
parent 94c1d4689c
commit f08b5632cc
4 changed files with 414 additions and 291 deletions

View File

@ -1,197 +1,214 @@
package net.simon987.server; package net.simon987.server;
import net.simon987.server.event.GameEvent;
import net.simon987.server.event.GameEvent; import net.simon987.server.event.GameEventDispatcher;
import net.simon987.server.event.GameEventDispatcher; import net.simon987.server.event.TickEvent;
import net.simon987.server.event.TickEvent; import net.simon987.server.game.GameUniverse;
import net.simon987.server.game.GameUniverse; import net.simon987.server.game.World;
import net.simon987.server.game.World; import net.simon987.server.logging.LogManager;
import net.simon987.server.logging.LogManager; import net.simon987.server.plugin.PluginManager;
import net.simon987.server.plugin.PluginManager; import net.simon987.server.plugin.ServerPlugin;
import net.simon987.server.plugin.ServerPlugin; import net.simon987.server.user.User;
import net.simon987.server.user.User; import net.simon987.server.webserver.SocketServer;
import net.simon987.server.webserver.SocketServer; import org.json.simple.JSONArray;
import org.json.simple.JSONArray; import org.json.simple.JSONObject;
import org.json.simple.JSONObject;
import java.io.File;
import java.io.File; import java.io.FileWriter;
import java.io.FileWriter; import java.io.IOException;
import java.io.IOException; import java.util.ArrayList;
import java.util.ArrayList;
public class GameServer implements Runnable {
public class GameServer implements Runnable {
public final static GameServer INSTANCE = new GameServer();
public final static GameServer INSTANCE = new GameServer();
private GameUniverse gameUniverse;
private GameUniverse gameUniverse; private GameEventDispatcher eventDispatcher;
private GameEventDispatcher eventDispatcher; private PluginManager pluginManager;
private PluginManager pluginManager;
private ServerConfiguration config;
private ServerConfiguration config;
private SocketServer socketServer;
private SocketServer socketServer;
private int maxExecutionTime;
private int maxExecutionTime;
public ArrayList<byte[]> saveArchive;
public GameServer() {
public int maxArchiveSize;
this.config = new ServerConfiguration(new File("config.properties"));
public GameServer() {
gameUniverse = new GameUniverse(config);
pluginManager = new PluginManager(); this.config = new ServerConfiguration(new File("config.properties"));
maxExecutionTime = config.getInt("user_timeout"); gameUniverse = new GameUniverse(config);
pluginManager = new PluginManager();
//Load all plugins in plugins folder, if it doesn't exist, create it
File pluginDir = new File("plugins/"); maxExecutionTime = config.getInt("user_timeout");
File[] pluginDirListing = pluginDir.listFiles();
// Load all plugins in plugins folder, if it doesn't exist, create it
if (pluginDirListing != null) { File pluginDir = new File("plugins/");
for (File pluginFile : pluginDirListing) { File[] pluginDirListing = pluginDir.listFiles();
if (pluginFile.getName().endsWith(".jar")) { if (pluginDirListing != null) {
pluginManager.load(pluginFile); for (File pluginFile : pluginDirListing) {
}
if (pluginFile.getName().endsWith(".jar")) {
} pluginManager.load(pluginFile);
} else { }
if (!pluginDir.mkdir()) {
LogManager.LOGGER.severe("Couldn't create plugin directory"); }
} } else {
} if (!pluginDir.mkdir()) {
LogManager.LOGGER.severe("Couldn't create plugin directory");
eventDispatcher = new GameEventDispatcher(pluginManager); }
}
}
eventDispatcher = new GameEventDispatcher(pluginManager);
public GameUniverse getGameUniverse() {
return gameUniverse; saveArchive = new ArrayList<byte[]>();
}
maxArchiveSize = config.getInt("max_archive_size");
public GameEventDispatcher getEventDispatcher() { }
return eventDispatcher;
} public GameUniverse getGameUniverse() {
return gameUniverse;
@Override }
public void run() {
LogManager.LOGGER.info("(G) Started game loop"); public GameEventDispatcher getEventDispatcher() {
return eventDispatcher;
long startTime; //Start time of the loop }
long uTime; //update time
long waitTime; //time to wait @Override
public void run() {
boolean running = true; LogManager.LOGGER.info("(G) Started game loop");
while (running) { long startTime; // Start time of the loop
long uTime; // update time
startTime = System.currentTimeMillis(); long waitTime; // time to wait
tick(); boolean running = true;
uTime = System.currentTimeMillis() - startTime; while (running) {
waitTime = config.getInt("tick_length") - uTime;
startTime = System.currentTimeMillis();
LogManager.LOGGER.info("Wait time : " + waitTime + "ms | Update time: " + uTime + "ms | " + (int) (((double) uTime / waitTime) * 100) + "% load");
tick();
try {
if (waitTime >= 0) { uTime = System.currentTimeMillis() - startTime;
Thread.sleep(waitTime); waitTime = config.getInt("tick_length") - uTime;
}
} catch (InterruptedException e) { LogManager.LOGGER.info("Wait time : " + waitTime + "ms | Update time: " + uTime + "ms | "
e.printStackTrace(); + (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
private void tick() {
gameUniverse.incrementTime();
//Process user code
ArrayList<User> users_ = gameUniverse.getUsers(); // Dispatch tick event
for (User user : users_) { GameEvent event = new TickEvent(gameUniverse.getTime());
GameServer.INSTANCE.getEventDispatcher().dispatch(event); // Ignore cancellation
if (user.getCpu() != null) {
try { // Process user code
ArrayList<User> users_ = gameUniverse.getUsers();
int timeout = Math.min(user.getControlledUnit().getEnergy(), maxExecutionTime); for (User user : users_) {
user.getCpu().reset(); if (user.getCpu() != null) {
int cost = user.getCpu().execute(timeout); try {
user.getControlledUnit().spendEnergy(cost);
int timeout = Math.min(user.getControlledUnit().getEnergy(), maxExecutionTime);
} catch (Exception e) {
LogManager.LOGGER.severe("Error executing " + user.getUsername() + "'s code"); user.getCpu().reset();
e.printStackTrace(); 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<World> worlds = new ArrayList<>(gameUniverse.getWorlds()); }
for (World world : worlds) { }
world.update();
} // Process each worlds
// Avoid concurrent modification
//Save ArrayList<World> worlds = new ArrayList<>(gameUniverse.getWorlds());
if (gameUniverse.getTime() % config.getInt("save_interval") == 0) { for (World world : worlds) {
save(new File("save.json")); world.update();
} }
socketServer.tick(); // Save
if (gameUniverse.getTime() % config.getInt("save_interval") == 0) {
LogManager.LOGGER.info("Processed " + gameUniverse.getWorlds().size() + " worlds"); save(new File("save.json"));
} }
/** socketServer.tick();
* Save game universe to file in JSON format
* LogManager.LOGGER.info("Processed " + gameUniverse.getWorlds().size() + " worlds");
* @param file JSON file to save }
*/
public void save(File file) { /**
* Save game universe to file in JSON format
try { *
FileWriter fileWriter = new FileWriter(file); * @param file
* JSON file to save
JSONObject universe = gameUniverse.serialise(); */
public void save(File file) {
JSONArray plugins = new JSONArray();
if (new File(new File("save.json").getAbsolutePath()).exists()) {
for (ServerPlugin plugin : pluginManager.getPlugins()) { saveArchive.add(ZipUtils.bytifyFile("save.json"));
plugins.add(plugin.serialise()); while(saveArchive.size() > maxArchiveSize) {
} saveArchive.remove(0);
}
universe.put("plugins", plugins); }
fileWriter.write(universe.toJSONString()); try {
fileWriter.close(); FileWriter fileWriter = new FileWriter(file);
LogManager.LOGGER.info("Saved to file " + file.getName()); JSONObject universe = gameUniverse.serialise();
} catch (IOException e) { JSONArray plugins = new JSONArray();
e.printStackTrace();
} for (ServerPlugin plugin : pluginManager.getPlugins()) {
plugins.add(plugin.serialise());
} }
public ServerConfiguration getConfig() { universe.put("plugins", plugins);
return config;
} fileWriter.write(universe.toJSONString());
fileWriter.close();
public PluginManager getPluginManager() {
return pluginManager; LogManager.LOGGER.info("Saved to file " + file.getName());
}
} catch (IOException e) {
public void setSocketServer(SocketServer socketServer) { e.printStackTrace();
this.socketServer = socketServer; }
}
} }
public ServerConfiguration getConfig() {
return config;
}
public PluginManager getPluginManager() {
return pluginManager;
}
public void setSocketServer(SocketServer socketServer) {
this.socketServer = socketServer;
}
public ArrayList<byte[]> getSaveArchive() {
return this.saveArchive;
}
}

View File

@ -1,29 +1,41 @@
package net.simon987.server; package net.simon987.server;
import net.simon987.server.logging.LogManager; import net.simon987.server.logging.LogManager;
import net.simon987.server.webserver.SocketServer; import net.simon987.server.webserver.SocketServer;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress; import java.io.IOException;
import java.net.InetSocketAddress;
public class Main {
public static void main(String[] args) { public class Main {
public static void main(String[] args) {
LogManager.initialize(); //Writes all of the files stored in GameServer.saveArray to a zip file.
ServerConfiguration config = new ServerConfiguration(new File("config.properties")); Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
//Load try {
GameServer.INSTANCE.getGameUniverse().load(new File("save.json")); ZipUtils.writeSavesToZip(GameServer.INSTANCE.getSaveArchive());
} catch (IOException e) {
SocketServer socketServer = new SocketServer(new InetSocketAddress(config.getString("webSocket_host"), System.out.println("Error writing saves to zip");
config.getInt("webSocket_port")), config); e.printStackTrace();
}
GameServer.INSTANCE.setSocketServer(socketServer); }
}, "Shutdown-thread"));
(new Thread(socketServer)).start(); LogManager.initialize();
(new Thread(GameServer.INSTANCE)).start(); 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();
}
}

View File

@ -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<byte[]> 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);
}
}

View File

@ -1,65 +1,68 @@
# MySQL username # MySQL username
mysql_user=mar mysql_user=mar
# MySQL password/ # MySQL password/
mysql_pass=mar mysql_pass=mar
# MySQL address # MySQL address
mysql_url=jdbc:mysql://localhost:3306/mar?useSSL=false mysql_url=jdbc:mysql://localhost:3306/mar?useSSL=false
save_interval=10 save_interval=10
# Web server port # Web server port
webSocket_port=8887 webSocket_port=8887
webSocket_host=0.0.0.0 webSocket_host=0.0.0.0
use_secure_webSocket=0 use_secure_webSocket=0
cert_path=certificates cert_path=certificates
# ---------------------------------------------- # ----------------------------------------------
# Length of a tick in ms # Length of a tick in ms
tick_length=1000 tick_length=1000
# Default offset of the origin (starting point of code execution) in words # Default offset of the origin (starting point of code execution) in words
org_offset=1024 org_offset=1024
# Address of the stack bottom # Address of the stack bottom
stack_bottom=32768 stack_bottom=32768
# Size of the memory in bytes # Size of the memory in bytes
memory_size=65536 memory_size=65536
# Initial location of new user's controlled unit # Initial location of new user's controlled unit
new_user_worldX = 32767 new_user_worldX = 32767
new_user_worldY = 32767 new_user_worldY = 32767
# Default user code # Default user code
new_user_code=; Welcome to Much Assembly required!\n\ 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\ ; You will find useful information on the game here: https://github.com/simon987/Much-Assembly-Required/wiki\n\
.text\n\ .text\n\
\t; Write code here\n\ \t; Write code here\n\
\tbrk \tbrk
# Default held item # Default held item
new_user_item=0 new_user_item=0
# ---------------------------------------------- # ----------------------------------------------
# Biomass units yield for a plant # Biomass units yield for a plant
plant_yield=2 plant_yield=2
# Grow time in ticks for a plant to grow # Grow time in ticks for a plant to grow
plant_grow_time=32 plant_grow_time=32
# Minimum tree count for the WorldGenerator # Minimum tree count for the WorldGenerator
minTreeCount=3 minTreeCount=3
# Maximum tree count for the WorldGenerator # Maximum tree count for the WorldGenerator
maxTreeCount=10 maxTreeCount=10
# Maximum energy of the battery hardware in kJ # Maximum energy of the battery hardware in kJ
battery_max_energy=60000 battery_max_energy=60000
# ---------------------------------------------- # ----------------------------------------------
# Minimum center point count for the WorldGenerator # Minimum center point count for the WorldGenerator
wg_centerPointCountMin=5 wg_centerPointCountMin=5
# Maximum center point count for the WorldGenerator # Maximum center point count for the WorldGenerator
wg_centerPointCountMax=15 wg_centerPointCountMax=15
# Wall/Plain tile ratio for the WorldGenerator # Wall/Plain tile ratio for the WorldGenerator
wg_wallPlainRatio=4 wg_wallPlainRatio=4
# Minimum iron tiles count for the WorldGenerator # Minimum iron tiles count for the WorldGenerator
wg_minIronCount=0 wg_minIronCount=0
# Minimum iron tile count for the WorldGenerator # Minimum iron tile count for the WorldGenerator
wg_maxIronCount=2 wg_maxIronCount=2
# Minimum copper tile count for the WorldGenerator # Minimum copper tile count for the WorldGenerator
wg_minCopperCount=0 wg_minCopperCount=0
# Maximum copper tile count for the WorldGenerator # Maximum copper tile count for the WorldGenerator
wg_maxCopperCount=2 wg_maxCopperCount=2
# ---------------------------------------------- # ----------------------------------------------
# Maximum execution time of user code in ms # Maximum execution time of user code in ms
user_timeout=500 user_timeout=500
# Free CPU execution time in ms # Free CPU execution time in ms
user_free_execution_time=2 user_free_execution_time=2
# ----------------------------------------------
# Max saves to archive when the server is shutdown
max_archive_size=10