mirror of
https://github.com/simon987/Much-Assembly-Required.git
synced 2025-04-10 14:26:45 +00:00
Implemented selective loading/unloading of worlds. Summary of changes:
1. Database is now iteratively updated, rather than dropped and recreated every time the universe is saved. 2. A single connection to the database is opened at server creation time, and used throughout. 3. Worlds, users and server information are now stored with appropriate IDs, so that they can be suitably updated. The world ID can be computed from world coordinates alone. 4. After saving, a world is unloaded from memory if it doesn't contain any updatable objects, and if all of its neighbours are either: (i) uncharted; (ii) not loaded in memory; (iii) without any updatable objects. This ensures that world unloading/reloading cannot be spammed by a single bot repeatedly transitioning in/out of an otherwise empty world. 5. The instance method GameUniverse.getWorld(int,int,boolean) first checks the world with given coordinates is in memory, then tries to load it from database, and only then creates a new one (if required by the boolean flag). 6. Worlds are now stored in a Hashtable indexed by world ID, for faster retrieval.
This commit is contained in:
parent
811a443a4e
commit
593be7624e
@ -36,11 +36,20 @@ public class GameServer implements Runnable {
|
||||
|
||||
private DayNightCycle dayNightCycle;
|
||||
|
||||
private MongoClient mongo = null;
|
||||
|
||||
public GameServer() {
|
||||
|
||||
this.config = new ServerConfiguration("config.properties");
|
||||
|
||||
try{
|
||||
mongo = new MongoClient("localhost", 27017);
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
gameUniverse = new GameUniverse(config);
|
||||
gameUniverse.setMongo(mongo);
|
||||
pluginManager = new PluginManager();
|
||||
|
||||
maxExecutionTime = config.getInt("user_timeout");
|
||||
@ -169,106 +178,101 @@ public class GameServer implements Runnable {
|
||||
") updated");
|
||||
}
|
||||
|
||||
|
||||
|
||||
void load() {
|
||||
|
||||
LogManager.LOGGER.info("Loading from MongoDB");
|
||||
MongoClient mongo;
|
||||
try {
|
||||
mongo = new MongoClient("localhost", 27017);
|
||||
LogManager.LOGGER.info("Loading all data from MongoDB");
|
||||
|
||||
DB db = mongo.getDB("mar");
|
||||
DB db = mongo.getDB("mar");
|
||||
|
||||
DBCollection worlds = db.getCollection("world");
|
||||
DBCollection users = db.getCollection("user");
|
||||
DBCollection server = db.getCollection("server");
|
||||
DBCollection worlds = db.getCollection("world");
|
||||
DBCollection users = db.getCollection("user");
|
||||
DBCollection server = db.getCollection("server");
|
||||
|
||||
//Load worlds
|
||||
DBCursor cursor = worlds.find();
|
||||
while (cursor.hasNext()) {
|
||||
GameServer.INSTANCE.getGameUniverse().getWorlds().add(World.deserialize(cursor.next()));
|
||||
}
|
||||
|
||||
//Load users
|
||||
cursor = users.find();
|
||||
while (cursor.hasNext()) {
|
||||
try {
|
||||
GameServer.INSTANCE.getGameUniverse().getUsers().add(User.deserialize(cursor.next()));
|
||||
} catch (CancelledException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
//Load misc server info
|
||||
cursor = server.find();
|
||||
if (cursor.hasNext()) {
|
||||
DBObject serverObj = cursor.next();
|
||||
gameUniverse.setTime((long) serverObj.get("time"));
|
||||
gameUniverse.setNextObjectId((long) serverObj.get("nextObjectId"));
|
||||
}
|
||||
|
||||
LogManager.LOGGER.info("Done loading! W:" + GameServer.INSTANCE.getGameUniverse().getWorlds().size() +
|
||||
" | U:" + GameServer.INSTANCE.getGameUniverse().getUsers().size());
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
BasicDBObject whereQuery = new BasicDBObject();
|
||||
whereQuery.put("shouldUpdate", true);
|
||||
DBCursor cursor = worlds.find(whereQuery);
|
||||
GameUniverse universe = GameServer.INSTANCE.getGameUniverse();
|
||||
while (cursor.hasNext()) {
|
||||
World w = World.deserialize(cursor.next());
|
||||
universe.addWorld(w);
|
||||
}
|
||||
|
||||
//Load users
|
||||
cursor = users.find();
|
||||
while (cursor.hasNext()) {
|
||||
try {
|
||||
universe.getUsers().add(User.deserialize(cursor.next()));
|
||||
} catch (CancelledException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
//Load misc server info
|
||||
cursor = server.find();
|
||||
if (cursor.hasNext()) {
|
||||
DBObject serverObj = cursor.next();
|
||||
gameUniverse.setTime((long) serverObj.get("time"));
|
||||
gameUniverse.setNextObjectId((long) serverObj.get("nextObjectId"));
|
||||
}
|
||||
|
||||
LogManager.LOGGER.info("Done loading! W:" + GameServer.INSTANCE.getGameUniverse().getWorlds().size() +
|
||||
" | U:" + GameServer.INSTANCE.getGameUniverse().getUsers().size());
|
||||
}
|
||||
|
||||
private void save() {
|
||||
|
||||
LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorlds().size() + " | U:" + gameUniverse.getUsers().size());
|
||||
|
||||
MongoClient mongo;
|
||||
try {
|
||||
mongo = new MongoClient("localhost", 27017);
|
||||
DB db = mongo.getDB("mar");
|
||||
|
||||
DB db = mongo.getDB("mar");
|
||||
int unloaded_worlds = 0;
|
||||
|
||||
db.dropDatabase(); //Todo: Update database / keep history instead of overwriting
|
||||
DBCollection worlds = db.getCollection("world");
|
||||
DBCollection users = db.getCollection("user");
|
||||
DBCollection server = db.getCollection("server");
|
||||
|
||||
DBCollection worlds = db.getCollection("world");
|
||||
DBCollection users = db.getCollection("user");
|
||||
DBCollection server = db.getCollection("server");
|
||||
|
||||
List<DBObject> worldDocuments = new ArrayList<>();
|
||||
int perBatch = 35;
|
||||
int insertedWorlds = 0;
|
||||
ArrayList<World> worlds_ = new ArrayList<>(GameServer.INSTANCE.getGameUniverse().getWorlds());
|
||||
for (World w : worlds_) {
|
||||
worldDocuments.add(w.mongoSerialise());
|
||||
insertedWorlds++;
|
||||
|
||||
if (worldDocuments.size() >= perBatch || insertedWorlds >= GameServer.INSTANCE.getGameUniverse().getWorlds().size()) {
|
||||
worlds.insert(worldDocuments);
|
||||
worldDocuments.clear();
|
||||
}
|
||||
}
|
||||
|
||||
List<DBObject> userDocuments = new ArrayList<>();
|
||||
int insertedUsers = 0;
|
||||
ArrayList<User> users_ = new ArrayList<>(GameServer.INSTANCE.getGameUniverse().getUsers());
|
||||
for (User u : users_) {
|
||||
|
||||
insertedUsers++;
|
||||
|
||||
if (!u.isGuest()) {
|
||||
userDocuments.add(u.mongoSerialise());
|
||||
}
|
||||
|
||||
if (userDocuments.size() >= perBatch || insertedUsers >= GameServer.INSTANCE.getGameUniverse().getUsers().size()) {
|
||||
users.insert(userDocuments);
|
||||
userDocuments.clear();
|
||||
}
|
||||
}
|
||||
|
||||
BasicDBObject serverObj = new BasicDBObject();
|
||||
serverObj.put("time", gameUniverse.getTime());
|
||||
serverObj.put("nextObjectId", gameUniverse.getNextObjectId());
|
||||
server.insert(serverObj);
|
||||
|
||||
LogManager.LOGGER.info("Done!");
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
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++;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
List<DBObject> userDocuments = new ArrayList<>();
|
||||
int insertedUsers = 0;
|
||||
ArrayList<User> users_ = new ArrayList<>(GameServer.INSTANCE.getGameUniverse().getUsers());
|
||||
for (User u : users_) {
|
||||
|
||||
insertedUsers++;
|
||||
|
||||
|
||||
if (!u.isGuest()) {
|
||||
users.save(u.mongoSerialise());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BasicDBObject serverObj = new BasicDBObject();
|
||||
serverObj.put("_id","serverinfo"); // a constant id ensures only one entry is kept and updated, instead of a new entry created every save.
|
||||
serverObj.put("time", gameUniverse.getTime());
|
||||
serverObj.put("nextObjectId", gameUniverse.getNextObjectId());
|
||||
server.save(serverObj);
|
||||
|
||||
LogManager.LOGGER.info(""+insertedWorlds+" worlds saved, "+unloaded_worlds+" unloaded");
|
||||
LogManager.LOGGER.info("Done!");
|
||||
}
|
||||
|
||||
public ServerConfiguration getConfig() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.simon987.server.game;
|
||||
|
||||
import com.mongodb.*;
|
||||
import net.simon987.server.GameServer;
|
||||
import net.simon987.server.ServerConfiguration;
|
||||
import net.simon987.server.assembly.Assembler;
|
||||
@ -9,14 +10,21 @@ 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;
|
||||
|
||||
public class GameUniverse {
|
||||
|
||||
private ArrayList<World> worlds;
|
||||
//private ArrayList<World> worlds;
|
||||
private Hashtable<String,World> worlds;
|
||||
private ArrayList<User> users;
|
||||
private WorldGenerator worldGenerator;
|
||||
|
||||
private MongoClient mongo = null;
|
||||
|
||||
|
||||
private long time;
|
||||
|
||||
private long nextObjectId = 0;
|
||||
@ -25,63 +33,149 @@ public class GameUniverse {
|
||||
|
||||
public GameUniverse(ServerConfiguration config) {
|
||||
|
||||
worlds = new ArrayList<>(32);
|
||||
//worlds = new ArrayList<>(32);
|
||||
worlds = new Hashtable<String,World>(32);
|
||||
users = new ArrayList<>(16);
|
||||
|
||||
worldGenerator = new WorldGenerator(config);
|
||||
}
|
||||
|
||||
public void setMongo(MongoClient mongo){
|
||||
this.mongo = mongo;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public World getWorld(int x, int y, boolean createNew) {
|
||||
/**
|
||||
* Attempts loading a world from mongoDB by coordinates
|
||||
*
|
||||
* @param x the x coordinate of the world
|
||||
* @param y the y coordinate of the world
|
||||
*
|
||||
* @return World, null if not found
|
||||
*/
|
||||
private World loadWorld(int x, int y){
|
||||
|
||||
DB db = mongo.getDB("mar");
|
||||
DBCollection worlds = db.getCollection("world");
|
||||
|
||||
for (World world : worlds) {
|
||||
if (world.getX() == x && world.getY() == y) {
|
||||
return world;
|
||||
}
|
||||
BasicDBObject whereQuery = new BasicDBObject();
|
||||
whereQuery.put("_id", World.idFromCoordinates(x,y));
|
||||
DBCursor cursor = worlds.find(whereQuery);
|
||||
if (cursor.hasNext()) {
|
||||
World w = World.deserialize(cursor.next());
|
||||
return w;
|
||||
}
|
||||
|
||||
if (x >= 0 && x <= maxWidth && y >= 0 && y <= maxWidth) {
|
||||
if (createNew) {
|
||||
//World does not exist
|
||||
World world = createWorld(x, y);
|
||||
worlds.add(world);
|
||||
|
||||
return world;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public World createWorld(int x, int y) {
|
||||
/**
|
||||
* Get a world by coordinates, attempts to load from mongoDB if not found.
|
||||
*
|
||||
* @param x the x coordinate of the world
|
||||
* @param y the y coordinate of the world
|
||||
* @param createNew if true, a new world is created when a world with given coordinates is not found
|
||||
*
|
||||
* @return World, null if not found and not created.
|
||||
*/
|
||||
public World getWorld(int x, int y, boolean createNew) {
|
||||
|
||||
// Wrapping coordinates around cyclically
|
||||
x %= maxWidth+1;
|
||||
y %= maxWidth+1;
|
||||
|
||||
// Looks for a previously loaded world
|
||||
World world = getLoadedWorld(x,y);
|
||||
if (world != null){
|
||||
return world;
|
||||
}
|
||||
|
||||
// Tries loading the world from the database
|
||||
world = loadWorld(x,y);
|
||||
if (world != null){
|
||||
addWorld(world);
|
||||
LogManager.LOGGER.fine("Loaded world "+(World.idFromCoordinates(x,y))+" from mongodb.");
|
||||
return world;
|
||||
}
|
||||
|
||||
// World does not exist
|
||||
if (createNew) {
|
||||
// Creates a new world
|
||||
world = createWorld(x, y);
|
||||
addWorld(world);
|
||||
LogManager.LOGGER.fine("Created new world "+(World.idFromCoordinates(x,y))+".");
|
||||
return world;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public World getLoadedWorld(int x, int y) {
|
||||
// Wrapping coordinates around cyclically
|
||||
x %= maxWidth+1;
|
||||
y %= maxWidth+1;
|
||||
|
||||
return worlds.get(World.idFromCoordinates(x,y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new or freshly loaded world to the universe (if not already present).
|
||||
*
|
||||
* @param world the world to be added
|
||||
*/
|
||||
public void addWorld(World world){
|
||||
World w = worlds.get(world.getId());
|
||||
if (w == null){
|
||||
world.setUniverse(this);
|
||||
worlds.put(world.getId(),world);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the world with given coordinates from the universe.
|
||||
*
|
||||
* @param x the x coordinate of the world to be removed
|
||||
* @param y the y coordinate of the world to be removed
|
||||
*/
|
||||
public void removeWorld(int x, int y){
|
||||
World w = worlds.remove(World.idFromCoordinates(x,y));
|
||||
if (w != null){
|
||||
w.setUniverse(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given world from the universe.
|
||||
*
|
||||
* @param world the world to be removed.
|
||||
*/
|
||||
public void removeWorld(World world){
|
||||
World w = worlds.remove(world.getId());
|
||||
if (w != null){
|
||||
w.setUniverse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public World createWorld(int x, int y) {
|
||||
World world = null;
|
||||
try {
|
||||
world = worldGenerator.generateWorld(x, y);
|
||||
|
||||
|
||||
} catch (CancelledException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
public User getUser(String username) {
|
||||
|
||||
for (User user : users) {
|
||||
if (user.getUsername().equals(username)) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -142,7 +236,7 @@ public class GameUniverse {
|
||||
public GameObject getObject(long id) {
|
||||
|
||||
//
|
||||
for (World world : worlds) {
|
||||
for (World world : getWorlds()) {
|
||||
for (GameObject object : world.getGameObjects()) {
|
||||
if (object.getObjectId() == id) {
|
||||
return object;
|
||||
@ -160,7 +254,7 @@ public class GameUniverse {
|
||||
}
|
||||
|
||||
public ArrayList<World> getWorlds() {
|
||||
return worlds;
|
||||
return new ArrayList<World>(worlds.values());
|
||||
}
|
||||
|
||||
public ArrayList<User> getUsers() {
|
||||
|
@ -6,9 +6,11 @@ import com.mongodb.DBObject;
|
||||
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;
|
||||
@ -57,7 +59,28 @@ public class World implements MongoSerialisable {
|
||||
public boolean isTileBlocked(int x, int y) {
|
||||
|
||||
return getGameObjectsBlockingAt(x, y).size() > 0 || tileMap.getTileAt(x, y) == TileMap.WALL_TILE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the world's unique id from its coordinates.
|
||||
*
|
||||
* @param x the x coordinate of the world
|
||||
* @param y the y coordinate of the world
|
||||
*
|
||||
* @return long
|
||||
*/
|
||||
public static String idFromCoordinates(int x, int y){
|
||||
return "w-"+"0x"+Integer.toHexString(x)+"-"+"0x"+Integer.toHexString(y);
|
||||
//return ((long)x)*(((long)maxWidth)+1)+((long)y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the world's unique id, computed with idFromCoordinates.
|
||||
*
|
||||
* @return long
|
||||
*/
|
||||
public String getId(){
|
||||
return World.idFromCoordinates(x,y);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
@ -124,6 +147,9 @@ public class World implements MongoSerialisable {
|
||||
objects.add(obj.mongoSerialise());
|
||||
}
|
||||
|
||||
|
||||
dbObject.put("_id", getId());
|
||||
|
||||
dbObject.put("objects", objects);
|
||||
dbObject.put("terrain", tileMap.mongoSerialise());
|
||||
|
||||
@ -131,7 +157,7 @@ public class World implements MongoSerialisable {
|
||||
dbObject.put("y", y);
|
||||
|
||||
dbObject.put("updatable", updatable);
|
||||
|
||||
dbObject.put("shouldUpdate",shouldUpdate());
|
||||
|
||||
return dbObject;
|
||||
}
|
||||
@ -330,4 +356,73 @@ public class World implements MongoSerialisable {
|
||||
public boolean shouldUpdate() {
|
||||
return updatable > 0;
|
||||
}
|
||||
|
||||
|
||||
private GameUniverse universe = null;
|
||||
|
||||
public void setUniverse(GameUniverse universe){
|
||||
this.universe = universe;
|
||||
}
|
||||
|
||||
public ArrayList<World> getNeighbouringLoadedWorlds(){
|
||||
ArrayList<World> neighbouringWorlds = new ArrayList<>();
|
||||
|
||||
if (universe == null){
|
||||
return neighbouringWorlds;
|
||||
}
|
||||
|
||||
for (int dx=-1; dx<=+1; dx+=2){
|
||||
World nw = universe.getLoadedWorld(x+dx,y);
|
||||
if (nw != null){
|
||||
neighbouringWorlds.add(nw);
|
||||
}
|
||||
}
|
||||
for (int dy=-1; dy<=+1; dy+=2){
|
||||
World nw = universe.getLoadedWorld(x,y+dy);
|
||||
if (nw != null){
|
||||
neighbouringWorlds.add(nw);
|
||||
}
|
||||
}
|
||||
|
||||
return neighbouringWorlds;
|
||||
}
|
||||
|
||||
public ArrayList<World> getNeighbouringExistingWorlds(){
|
||||
ArrayList<World> neighbouringWorlds = new ArrayList<>();
|
||||
|
||||
if (universe == null){
|
||||
return neighbouringWorlds;
|
||||
}
|
||||
|
||||
for (int dx=-1; dx<=+1; dx+=2){
|
||||
World nw = universe.getWorld(x+dx,y,false);
|
||||
if (nw != null){
|
||||
neighbouringWorlds.add(nw);
|
||||
}
|
||||
}
|
||||
for (int dy=-1; dy<=+1; dy+=2){
|
||||
World nw = universe.getWorld(x,y+dy,false);
|
||||
if (nw != null){
|
||||
neighbouringWorlds.add(nw);
|
||||
}
|
||||
}
|
||||
|
||||
return neighbouringWorlds;
|
||||
}
|
||||
|
||||
|
||||
public boolean canUnload(){
|
||||
return updatable==0;
|
||||
}
|
||||
|
||||
public boolean shouldUnload(){
|
||||
boolean res = canUnload();
|
||||
|
||||
for (World nw : getNeighbouringLoadedWorlds() ){
|
||||
res &= nw.canUnload();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ public class User implements MongoSerialisable {
|
||||
|
||||
BasicDBObject dbObject = new BasicDBObject();
|
||||
|
||||
dbObject.put("_id", username); // a constant id ensures only one entry per user is kept and updated, instead of a new entry created every save for every user.
|
||||
dbObject.put("username", username);
|
||||
dbObject.put("code", userCode);
|
||||
dbObject.put("controlledUnit", controlledUnit.getObjectId());
|
||||
|
Loading…
x
Reference in New Issue
Block a user