Fixes #121 + Saner thread safety

This commit is contained in:
simon 2018-01-09 22:10:55 -05:00
parent 59fd620e4a
commit 54b72e89b3
13 changed files with 128 additions and 289 deletions

View File

@ -37,7 +37,7 @@ public class UserCreationListener implements GameEventListener {
cubot.setX(point.x);
cubot.setY(point.y);
cubot.getWorld().getGameObjects().add(cubot);
cubot.getWorld().addObject(cubot);
cubot.getWorld().incUpdatable();
cubot.setHeldItem(GameServer.INSTANCE.getConfig().getInt("new_user_item"));

View File

@ -74,7 +74,7 @@ public class Factory extends GameObject implements Updatable {
npc.setObjectId(GameServer.INSTANCE.getGameUniverse().getNextObjectId());
npc.setX(p.x);
npc.setY(p.y);
getWorld().getGameObjects().add(npc);
getWorld().addObject(npc);
getWorld().incUpdatable();
npc.setFactory(this);

View File

@ -33,14 +33,9 @@ public class HarvestTask extends NPCTask {
if (pause == 0) {
//Get biomass
ArrayList<GameObject> biomass = new ArrayList<>(10);
for (GameObject object : npc.getWorld().getGameObjects()) {
//Plant MAP_INFO
if ((object.getMapInfo() & 0x4000) == 0x4000) {
biomass.add(object);
}
}
/* todo replace by some sort of .collect call with object
id (See https://github.com/simon987/Much-Assembly-Required/pull/66)*/
ArrayList<GameObject> biomass = npc.getWorld().findObjects(0x4000);
//Get closest one
int minDist = Integer.MAX_VALUE;

View File

@ -53,7 +53,7 @@ public class WorldCreationListener implements GameEventListener {
continue;
}
world.getGameObjects().add(factory);
world.addObject(factory);
world.incUpdatable();
LogManager.LOGGER.info("Spawned Factory at (" + world.getX() + ", " + world.getY() +
@ -84,7 +84,7 @@ public class WorldCreationListener implements GameEventListener {
if (radioTower.getAdjacentTile() != null) {
//Radio Tower has adjacent tiles
world.getGameObjects().add(radioTower);
world.addObject(radioTower);
world.incUpdatable(); //In case the Factory couldn't be spawned.
NpcPlugin.getRadioTowers().add(radioTower);

View File

@ -25,7 +25,9 @@ public class WorldCreationListener implements GameEventListener {
ArrayList<BiomassBlob> biomassBlobs = WorldUtils.generateBlobs(((WorldGenerationEvent) event).getWorld(),
minCount, maxCount, yield);
((WorldGenerationEvent) event).getWorld().getGameObjects().addAll(biomassBlobs);
for (BiomassBlob blob : biomassBlobs) {
((WorldGenerationEvent) event).getWorld().addObject(blob);
}
}
}

View File

@ -17,11 +17,11 @@ public class WorldUpdateListener implements GameEventListener {
private HashMap<World, Long> worldWaitMap = new HashMap<>(200);
private int minBlobCount;
private int maxBlobCount;
private int blobYield;
private int waitTime;
private int blobThreshold;
private static int minBlobCount;
private static int maxBlobCount;
private static int blobYield;
private static int waitTime;
private static int blobThreshold;
public WorldUpdateListener(ServerConfiguration config) {
@ -45,7 +45,7 @@ public class WorldUpdateListener implements GameEventListener {
World world = ((WorldUpdateEvent) event).getWorld();
//If there is less than the respawn threshold,
if (world.getGameObjects(BiomassBlob.class).size() < blobThreshold) {
if (world.findObjects(BiomassBlob.class).size() < blobThreshold) {
//Set a timer for respawn_time ticks
if (!worldWaitMap.containsKey(world) || worldWaitMap.get(world) == 0L) {
@ -59,7 +59,9 @@ public class WorldUpdateListener implements GameEventListener {
//If the timer was set less than respawn_time ticks ago, respawn the blobs
ArrayList<BiomassBlob> newBlobs = WorldUtils.generateBlobs(world, minBlobCount,
maxBlobCount, blobYield);
world.getGameObjects().addAll(newBlobs);
for (BiomassBlob blob : newBlobs) {
world.addObject(blob);
}
//Set the 'waitUntil' time to 0 to indicate that we are not waiting
worldWaitMap.replace(world, 0L);

View File

@ -9,7 +9,6 @@ import net.simon987.server.event.TickEvent;
import net.simon987.server.game.DayNightCycle;
import net.simon987.server.game.GameUniverse;
import net.simon987.server.game.World;
import net.simon987.server.io.FileUtils;
import net.simon987.server.logging.LogManager;
import net.simon987.server.plugin.PluginManager;
import net.simon987.server.user.User;
@ -17,8 +16,6 @@ import net.simon987.server.webserver.SocketServer;
import java.io.File;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
public class GameServer implements Runnable {
@ -131,8 +128,7 @@ public class GameServer implements Runnable {
//Process user code
ArrayList<User> users_ = new ArrayList<>(gameUniverse.getUsers());
for (User user : users_) {
for (User user : gameUniverse.getUsers()) {
if (user.getCpu() != null) {
try {
@ -152,10 +148,8 @@ public class GameServer implements Runnable {
}
//Process each worlds
//Avoid concurrent modification
ArrayList<World> worlds = new ArrayList<>(gameUniverse.getWorlds());
int updatedWorlds = 0;
for (World world : worlds) {
for (World world : gameUniverse.getWorlds()) {
if (world.shouldUpdate()) {
world.update();
updatedWorlds++;
@ -167,14 +161,9 @@ public class GameServer implements Runnable {
save();
}
// Clean up history files
if (gameUniverse.getTime() % config.getInt("clean_interval") == 0) {
FileUtils.cleanHistory(config.getInt("history_size"));
}
socketServer.tick();
LogManager.LOGGER.info("Processed " + gameUniverse.getWorlds().size() + " worlds (" + updatedWorlds +
LogManager.LOGGER.info("Processed " + gameUniverse.getWorldCount() + " worlds (" + updatedWorlds +
") updated");
}
@ -203,7 +192,7 @@ public class GameServer implements Runnable {
cursor = users.find();
while (cursor.hasNext()) {
try {
universe.getUsers().add(User.deserialize(cursor.next()));
universe.addUser(User.deserialize(cursor.next()));
} catch (CancelledException e) {
e.printStackTrace();
}
@ -217,13 +206,13 @@ public class GameServer implements Runnable {
gameUniverse.setNextObjectId((long) serverObj.get("nextObjectId"));
}
LogManager.LOGGER.info("Done loading! W:" + GameServer.INSTANCE.getGameUniverse().getWorlds().size() +
" | U:" + GameServer.INSTANCE.getGameUniverse().getUsers().size());
LogManager.LOGGER.info("Done loading! W:" + GameServer.INSTANCE.getGameUniverse().getWorldCount() +
" | U:" + GameServer.INSTANCE.getGameUniverse().getUserCount());
}
private void save() {
LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorlds().size() + " | U:" + gameUniverse.getUsers().size());
LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorldCount() + " | U:" + gameUniverse.getUserCount());
try{
DB db = mongo.getDB("mar");
@ -233,31 +222,22 @@ public class GameServer implements Runnable {
DBCollection users = db.getCollection("user");
DBCollection server = db.getCollection("server");
List<DBObject> worldDocuments = new ArrayList<>();
int perBatch = 35;
int insertedWorlds = 0;
GameUniverse universe = GameServer.INSTANCE.getGameUniverse();
ArrayList<World> worlds_ = new ArrayList<>(universe.getWorlds());
for (World w : worlds_) {
LogManager.LOGGER.fine("Saving world "+w.getId()+" to mongodb");
insertedWorlds++;
for (World w : universe.getWorlds()) {
// LogManager.LOGGER.fine("Saving world "+w.getId()+" to mongodb");
insertedWorlds++;
worlds.save(w.mongoSerialise());
// If the world should unload, it is removed from the Universe after having been saved.
if (w.shouldUnload()){
unloaded_worlds++;
LogManager.LOGGER.fine("Unloading world "+w.getId()+" from universe");
universe.removeWorld(w);
// LogManager.LOGGER.fine("Unloading world "+w.getId()+" from universe");
universe.removeWorld(w);
}
}
List<DBObject> userDocuments = new ArrayList<>();
int insertedUsers = 0;
ArrayList<User> users_ = new ArrayList<>(GameServer.INSTANCE.getGameUniverse().getUsers());
for (User u : users_) {
insertedUsers++;
for (User u : GameServer.INSTANCE.getGameUniverse().getUsers()) {
if (!u.isGuest()) {
users.save(u.mongoSerialise());

View File

@ -84,9 +84,9 @@ public abstract class GameObject implements JSONSerialisable, MongoSerialisable
}
if (leftWorld != null) {
world.getGameObjects().remove(this);
world.removeObject(this);
world.decUpdatable();
leftWorld.getGameObjects().add(this);
leftWorld.addObject(this);
leftWorld.incUpdatable();
setWorld(leftWorld);
@ -103,9 +103,9 @@ public abstract class GameObject implements JSONSerialisable, MongoSerialisable
}
if (rightWorld != null) {
world.getGameObjects().remove(this);
world.removeObject(this);
world.decUpdatable();
rightWorld.getGameObjects().add(this);
rightWorld.addObject(this);
rightWorld.incUpdatable();
setWorld(rightWorld);
@ -123,9 +123,9 @@ public abstract class GameObject implements JSONSerialisable, MongoSerialisable
}
if (upWorld != null) {
world.getGameObjects().remove(this);
world.removeObject(this);
world.decUpdatable();
upWorld.getGameObjects().add(this);
upWorld.addObject(this);
upWorld.incUpdatable();
setWorld(upWorld);
@ -143,9 +143,9 @@ public abstract class GameObject implements JSONSerialisable, MongoSerialisable
if (downWorld != null) {
world.getGameObjects().remove(this);
world.removeObject(this);
world.decUpdatable();
downWorld.getGameObjects().add(this);
downWorld.addObject(this);
downWorld.incUpdatable();
setWorld(downWorld);

View File

@ -10,16 +10,15 @@ import net.simon987.server.assembly.exception.CancelledException;
import net.simon987.server.logging.LogManager;
import net.simon987.server.user.User;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
public class GameUniverse {
//private ArrayList<World> worlds;
private Hashtable<String,World> worlds;
private ArrayList<User> users;
private ConcurrentHashMap<String, World> worlds;
//username:user
private ConcurrentHashMap<String, User> users;
private WorldGenerator worldGenerator;
private MongoClient mongo = null;
@ -33,9 +32,8 @@ public class GameUniverse {
public GameUniverse(ServerConfiguration config) {
//worlds = new ArrayList<>(32);
worlds = new Hashtable<String,World>(32);
users = new ArrayList<>(16);
worlds = new ConcurrentHashMap<>(256);
users = new ConcurrentHashMap<>(16);
worldGenerator = new WorldGenerator(config);
}
@ -160,7 +158,7 @@ public class GameUniverse {
}
}
public World createWorld(int x, int y) {
private World createWorld(int x, int y) {
World world = null;
try {
world = worldGenerator.generateWorld(x, y);
@ -171,12 +169,7 @@ public class GameUniverse {
}
public User getUser(String username) {
for (User user : users) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
return users.get(username);
}
public User getOrCreateUser(String username, boolean makeControlledUnit) {
@ -213,7 +206,7 @@ public class GameUniverse {
user.setUsername(username);
users.add(user);
addUser(user);
return user;
@ -235,12 +228,11 @@ public class GameUniverse {
*/
public GameObject getObject(long id) {
//
for (World world : getWorlds()) {
for (GameObject object : world.getGameObjects()) {
if (object.getObjectId() == id) {
return object;
}
GameObject obj = world.findObject(id);
if (obj != null) {
return obj;
}
}
@ -253,12 +245,20 @@ public class GameUniverse {
time++;
}
public ArrayList<World> getWorlds() {
return new ArrayList<World>(worlds.values());
public Collection<World> getWorlds() {
return worlds.values();
}
public ArrayList<User> getUsers() {
return users;
public int getWorldCount() {
return worlds.size();
}
public Collection<User> getUsers() {
return users.values();
}
public int getUserCount() {
return users.size();
}
public long getNextObjectId() {
@ -281,8 +281,12 @@ public class GameUniverse {
}
public void addUser(User user) {
users.put(user.getUsername(), user);
}
public void removeUser(User user) {
users.remove(user);
users.remove(user.getUsername());
}
public int getMaxWidth() {

View File

@ -3,18 +3,19 @@ package net.simon987.server.game;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.sun.istack.internal.Nullable;
import net.simon987.server.GameServer;
import net.simon987.server.event.GameEvent;
import net.simon987.server.event.WorldUpdateEvent;
import net.simon987.server.game.GameUniverse;
import net.simon987.server.game.pathfinding.Pathfinder;
import net.simon987.server.io.MongoSerialisable;
import org.json.simple.JSONObject;
import net.simon987.server.logging.LogManager;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class World implements MongoSerialisable {
@ -32,7 +33,7 @@ public class World implements MongoSerialisable {
private TileMap tileMap;
private ArrayList<GameObject> gameObjects = new ArrayList<>(16);
private ConcurrentHashMap<Long, GameObject> gameObjects = new ConcurrentHashMap<>(8);
/**
* If this number is greater than 0, the World will be updated.
@ -91,26 +92,49 @@ public class World implements MongoSerialisable {
return y;
}
/**
* Get all the game objects that are instances of the specified class
*/
public ArrayList getGameObjects(Class<? extends GameObject> clazz) {
public ArrayList<GameObject> findObjects(Class clazz) {
ArrayList<GameObject> objects = new ArrayList<>(gameObjects.size());
ArrayList<GameObject> matchingObjects = new ArrayList<>(2);
for (GameObject object : gameObjects) {
if (object.getClass().equals(clazz)) {
objects.add(object);
for (GameObject obj : gameObjects.values()) {
if (obj.getClass().equals(clazz)) {
matchingObjects.add(obj);
}
}
return objects;
return matchingObjects;
}
public ArrayList<GameObject> getGameObjects() {
return gameObjects;
public ArrayList<GameObject> findObjects(int mapInfo) {
ArrayList<GameObject> matchingObjects = new ArrayList<>(2);
for (GameObject obj : gameObjects.values()) {
if ((obj.getMapInfo() & mapInfo) == mapInfo) {
matchingObjects.add(obj);
}
}
return matchingObjects;
}
public void addObject(GameObject object) {
gameObjects.put(object.getObjectId(), object);
}
public void removeObject(GameObject object) {
gameObjects.remove(object.getObjectId());
}
@Nullable
public GameObject findObject(long objectId) {
return gameObjects.get(objectId);
}
/**
* Update this World and its GameObjects
* <br>
@ -122,13 +146,11 @@ public class World implements MongoSerialisable {
GameEvent event = new WorldUpdateEvent(this);
GameServer.INSTANCE.getEventDispatcher().dispatch(event); //Ignore cancellation
ArrayList<GameObject> gameObjects_ = new ArrayList<>(gameObjects);
for (GameObject object : gameObjects_) {
for (GameObject object : gameObjects.values()) {
//Clean up dead objects
if (object.isDead()) {
object.onDeadCallback();
gameObjects.remove(object);
removeObject(object);
//LogManager.LOGGER.fine("Removed object " + object + " id: " + object.getObjectId());
} else if (object instanceof Updatable) {
((Updatable) object).update();
@ -142,8 +164,7 @@ public class World implements MongoSerialisable {
BasicDBObject dbObject = new BasicDBObject();
BasicDBList objects = new BasicDBList();
ArrayList<GameObject> gameObjects_ = new ArrayList<>(gameObjects);
for (GameObject obj : gameObjects_) {
for (GameObject obj : gameObjects.values()) {
objects.add(obj.mongoSerialise());
}
@ -215,7 +236,7 @@ public class World implements MongoSerialisable {
GameObject object = GameObject.deserialize((DBObject) obj);
object.setWorld(world);
world.gameObjects.add(object);
world.addObject(object);
}
return world;
@ -254,7 +275,7 @@ public class World implements MongoSerialisable {
}
//Objects
for (GameObject obj : this.gameObjects) {
for (GameObject obj : gameObjects.values()) {
mapInfo[obj.getX()][obj.getY()] |= obj.getMapInfo();
}
@ -308,17 +329,16 @@ public class World implements MongoSerialisable {
*/
public ArrayList<GameObject> getGameObjectsBlockingAt(int x, int y) {
ArrayList<GameObject> gameObjects = new ArrayList<>(2);
for (GameObject obj : this.gameObjects) {
ArrayList<GameObject> objectsLooking = new ArrayList<>(2);
for (GameObject obj : gameObjects.values()) {
if (obj.isAt(x, y)) {
gameObjects.add(obj);
objectsLooking.add(obj);
}
}
return gameObjects;
return objectsLooking;
}
/**
@ -332,17 +352,15 @@ public class World implements MongoSerialisable {
* @return the list of game objects at a location
*/
public ArrayList<GameObject> getGameObjectsAt(int x, int y) {
ArrayList<GameObject> gameObjects = new ArrayList<>(2);
ArrayList<GameObject> objectsAt = new ArrayList<>(2);
for (GameObject obj : gameObjects.values()) {
for (GameObject obj : this.gameObjects) {
if (obj.getX() == x && obj.getY() == y) {
gameObjects.add(obj);
if (obj.isAt(x, y)) {
objectsAt.add(obj);
}
}
return gameObjects;
return objectsAt;
}
public void incUpdatable() {
@ -425,4 +443,7 @@ public class World implements MongoSerialisable {
return res;
}
public Collection<GameObject> getGameObjects() {
return gameObjects.values();
}
}

View File

@ -1,35 +1,12 @@
package net.simon987.server.io;
import net.simon987.server.logging.LogManager;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class FileUtils {
private static final int BUFFER_SIZE = 1024;
private static final String STR_ENCODING = "UTF-8";
private static final String DATE_FORMAT = "yyyyMMddHHmmss";
private static final String FILE_TYPE = ".zip";
private static final Path ROOT_DIR;
private static final String DIR_NAME = "history";
public static final Path DIR_PATH;
static {
ROOT_DIR = Paths.get(".").normalize();
DIR_PATH = ROOT_DIR.resolve(DIR_NAME);
}
//Private constructor
private FileUtils() {
}
/**
* Creates a new stamp containing the current date and time
@ -42,143 +19,4 @@ public class FileUtils {
return f.format(millisToDate);
}
/**
* Created a directory if none exists with the specified name
*
* @param directory folder to create
* @return true is the file exists or create operation is successful
*/
public static boolean prepDirectory(Path directory) {
File file = directory.toFile();
//If the directory exists or the directory created successfully return true
if (file.exists() || file.mkdir()) {
return true;
} else {
System.out.println("Error creating directory: " + file.toString());
return false;
}
}
/**
* Converts a file into an array of bytes
*
* @param path the file to be converted into bytes
* @return the byte array of the given file
*/
public static byte[] bytifyFile(Path path) {
byte[] bytes = null;
try {
bytes = Files.readAllBytes(path);
} catch (IOException e) {
System.out.println("Failed to extract bytes from: " + path);
e.printStackTrace();
}
return bytes;
}
/**
* Takes in a file that had been converted to a byte[] to be written to a new
* zip file
*
* @param data
* contains data in byte array form to be written, typically a file
* that has been converted with bytifyFile()
* @throws IOException
* if an error occurs during the write process
*/
public static void writeSaveToZip(String name, byte[] data) throws IOException {
String newFile = DIR_PATH.resolve(getDateTimeStamp() + FILE_TYPE).toString();
FileOutputStream output = new FileOutputStream(newFile);
ZipOutputStream stream = new ZipOutputStream(output);
byte[] buffer = new byte[BUFFER_SIZE];
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
while ((bais.read(buffer)) > -1) {
// File name
ZipEntry entry = new ZipEntry(name);
// Set to start of next entry in the stream.
stream.putNextEntry(entry);
// Data to write.
stream.write(data);
// Close the current entry.
stream.closeEntry();
}
stream.close();
output.close();
}
public static void cleanHistory(int size) {
File[] files = new File(DIR_PATH.toString()).listFiles();
File[] sorted = new File[size];
File currentFile;
boolean changed;
if (files != null) {
for (int i = 0; i < files.length / 2; i++) {
currentFile = files[i];
files[i] = files[files.length - i - 1];
files[files.length - i - 1] = currentFile;
}
for (int f = 0; f < files.length; f++) {
changed = false;
try {
long dirFile = Long.parseLong(files[f].getName().substring(0, (files[f].getName().length() - 4)));
if (f < size && sorted[f] == null) {
sorted[f] = files[f];
} else {
for (int s = 0; s < sorted.length; s++) {
long sortedFile = Long.parseLong(sorted[s].getName().substring(0, (sorted[s].getName().length() - 4)));
if (dirFile > sortedFile) {
if (s == sorted.length - 1) {
sorted[s] = files[f];
} else {
sorted[s] = files[f];
}
changed = true;
}
}
if (!changed) {
files[f].delete();
}
}
} catch (NumberFormatException e) {
LogManager.LOGGER.info("Non-save file in history directory: " + files[f].getName());
}
}
}
}
/**
* Converts a byte array into human readable format using the provided encoding
*
* @param bytes data to be encoded to String
* @return a String containing the encoded bytes
*/
public static String byteArrAsString(byte[] bytes) throws UnsupportedEncodingException {
return new String(bytes, STR_ENCODING);
}
}

View File

@ -7,8 +7,6 @@ import net.simon987.server.logging.LogManager;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import java.util.ArrayList;
public class ObjectsRequestHandler implements MessageHandler {
@ -29,13 +27,12 @@ public class ObjectsRequestHandler implements MessageHandler {
World world = GameServer.INSTANCE.getGameUniverse().getWorld(x, y, false);
if (world != null) {
ArrayList<GameObject> gameObjects = world.getGameObjects();
JSONObject response = new JSONObject();
JSONArray objects = new JSONArray();
for (GameObject object : gameObjects) {
for (GameObject object : world.getGameObjects()) {
objects.add(object.serialise());
}

View File

@ -17,7 +17,7 @@ public class TerrainRequestHandler implements MessageHandler {
try {
world = GameServer.INSTANCE.getGameUniverse().getWorld(
Long.valueOf((long) json.get("x")).intValue(),
Long.valueOf((long) json.get("y")).intValue(), false);
Long.valueOf((long) json.get("y")).intValue(), true);
} catch (NullPointerException e) {
LogManager.LOGGER.severe("FIXME: handle TerrainRequestHandler");
return;