package net.kennux.cubicworld;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Stack;
import net.kennux.cubicworld.entity.AEntity;
import net.kennux.cubicworld.entity.EntityManager;
import net.kennux.cubicworld.entity.EntitySystem;
import net.kennux.cubicworld.environment.DayNightCycle;
import net.kennux.cubicworld.networking.CubicWorldServerClient;
import net.kennux.cubicworld.networking.IPacketModel;
import net.kennux.cubicworld.networking.packet.ServerEntityDestroy;
import net.kennux.cubicworld.networking.packet.ServerVoxelUpdate;
import net.kennux.cubicworld.profiler.Profiler;
import net.kennux.cubicworld.profiler.Profiler.FileFormat;
import net.kennux.cubicworld.serialization.BitReader;
import net.kennux.cubicworld.util.ConsoleHelper;
import net.kennux.cubicworld.voxel.VoxelData;
import net.kennux.cubicworld.voxel.VoxelWorld;
import net.kennux.cubicworld.voxel.VoxelWorldSave;
import net.kennux.cubicworld.voxel.generator.TestGenerator;
import net.kennux.cubicworld.voxel.generator.WorldGenerator;
import net.kennux.cubicworld.voxel.generator.noise.SimplexNoise3D;
import net.kennux.cubicworld.voxel.handlers.IVoxelDataUpdateHandler;
import com.badlogic.gdx.math.Vector3;
/**
* The cubic world server implementation.
*
* @author KennuX
*
*/
public class CubicWorldServer implements Runnable
{
/**
* The libgdx server socket instance.
*/
private ServerSocket serverSocket;
/**
* The server thread. It will run the run() method.
*/
private Thread serverThread;
/**
* The update thread.
*/
private Thread updateThread;
/**
* The voxel world instance.
*/
public VoxelWorld voxelWorld;
/**
* Ticks per second to execute.
*/
public final int ticksPerSecond = 20;
/**
* The current tick.
* Gets incremented after every tick.
*/
public int tick = 0;
/**
* The clients array contains all connected clients. If you access this
* value, you must lock / synchronize the clientsLockObject.
*/
public CubicWorldServerClient[] clients;
public Object clientsLockObject = new Object();
/**
* The current stack of packets to send in this frame. If you access this
* value, you must lock / synchronize the packetStackLockObject.
*/
public Stack<IPacketModel> packets;
public Object packetStackLockObject = new Object();
/**
* The entity manager.
*/
public EntityManager entityManager;
public Object entityManagerLockObject = new Object();
/**
* Day night cycle instance, will get initialized before the server update
* thread starts.
*/
public DayNightCycle dayNightCycle;
/**
* The profiler instance used by this instance.
*/
public Profiler profiler;
/**
* The deltatime of the server.
*/
public float deltaTime;
/**
* The plugin manager of this server.
*/
public PluginManager pluginManager;
/**
* The path to the folder where the savedata is located with ending slash.
*/
public String savePath;
/**
* The save thread which will handle the server saves.
*/
private Thread saveThread;
/**
* If this gets set to false, server threads will stop.
*/
private boolean isRunning;
/**
* <pre>
* Initializes the server socket, starts listening.
* Initialization order:
*
* - Singleton pattern
* - Profiler
* - Server Socket
* - Server Thread (Listener)
* - Bootstrap
* - Packet stack
* - Voxel World
* - Entity manager
* - Spawn area preparation (generating all chunks around 0|0|0 based on chunkViewDistance).
* - Update thread
* - Daynight cycle
* </pre>
*
* @param protocol
* @param port
* @param version
*/
public CubicWorldServer(short port, String version, int slots)
{
if (CubicWorld.getServer() != null)
{
ConsoleHelper.writeLog("ERROR", "Server instance already initialized!", "Server Init");
System.exit(-1);
}
CubicWorld.setServer(this);
ConsoleHelper.writeLog("info", "Initializing CubicWorldServer...", "Server Init");
// Init profiler
this.profiler = new Profiler();
try
{
this.profiler.openProfilingFile("server_profilings.txt", FileFormat.PLAINTEXT);
}
catch (IOException e)
{
ConsoleHelper.writeLog("error", "Couldn't initialize profiler!", "Server Profiler");
ConsoleHelper.logError(e);
}
this.profiler.startProfiling("ServerInit()", "");
// Init save path
this.savePath = "world/";
this.prepareSaveStructure();
// Init socket
try
{
this.serverSocket = new ServerSocket(port);
}
catch (IOException e)
{
ConsoleHelper.writeLog("error", "Couldn't initialize server socket on port " + port + "\r\n" + e.getMessage(), "Server Init");
return;
}
this.serverThread = new Thread(this);
ConsoleHelper.writeLog("info", "Executing bootstrap.", "Server Init");
this.pluginManager = new PluginManager();
ServerBootstrap.bootstrap(this.pluginManager);
ConsoleHelper.writeLog("info", "Bootstrap executed.", "Server Init");
// Create tick update thread
this.packets = new Stack<IPacketModel>();
// Init world
this.voxelWorld = new VoxelWorld(this);
this.voxelWorld.setVoxelDataUpdateHandler(new IVoxelDataUpdateHandler()
{
/**
* Constructs a voxel update packet and adds it to the server packet
* quene.
*/
@Override
public void handleVoxelDataUpdate(int x, int y, int z, VoxelData newData)
{
// Construct chunk update
ServerVoxelUpdate voxelUpdate = new ServerVoxelUpdate();
voxelUpdate.setCullPosition(new Vector3(x, y, z));
voxelUpdate.x = x;
voxelUpdate.y = y;
voxelUpdate.z = z;
voxelUpdate.voxel = newData;
CubicWorld.getServer().addPacket(voxelUpdate);
}
});
SimplexNoise3D.seed(1337);
this.voxelWorld.setWorldGenerator(new WorldGenerator());
this.entityManager = new EntityManager(this.voxelWorld, true, true, slots);
ConsoleHelper.writeLog("info", "Preparing spawn area...", "Server Init");
try
{
this.voxelWorld.setWorldFile(new VoxelWorldSave(this.savePath));
}
catch (Exception e)
{
ConsoleHelper.writeLog("ERROR", "Voxel world save file initialization failed: ", "Server");
ConsoleHelper.logError(e);
System.exit(-1);
}
this.voxelWorld.generateChunksAround(Vector3.Zero, CubicWorldConfiguration.chunkLoadDistance, true);
this.voxelWorld.update();
// load entity manager save
File entityFile = new File(this.savePath + "entities.dat");
if (entityFile.length() > 0)
{
try
{
// Read entity save data
FileInputStream entityFileIs = new FileInputStream(entityFile);
byte[] data = new byte[(int) entityFile.length()];
entityFileIs.read(data);
// Deserialize
this.entityManager.deserialize(new BitReader(data));
entityFileIs.close();
}
catch (Exception e)
{
ConsoleHelper.writeLog("ERROR", "Entity save reading failed: ", "Server");
ConsoleHelper.logError(e);
System.exit(-1);
}
}
ConsoleHelper.writeLog("info", "Spawn area prepared! Server running!", "Server Init");
// Init client socket
this.clients = new CubicWorldServerClient[slots];
// Update thread init
this.updateThread = new Thread(new CubicWorldServerUpdateThread(this));
this.updateThread.setName("Server update thread");
// init day night cycle
this.dayNightCycle = new DayNightCycle();
this.dayNightCycle.setTime((byte) 6, (byte) 0);
this.profiler.stopProfiling("ServerInit()");
// Init save thread
this.saveThread = new Thread(new CubicWorldServerSaveThread(this));
// Start threads
this.isRunning = true;
this.serverThread.start();
this.updateThread.start();
this.saveThread.start();
}
/**
* Adds a packet to the quene to send in the current frame.
*/
public void addPacket(IPacketModel packet)
{
synchronized (packetStackLockObject)
{
this.packets.push(packet);
}
}
/**
* <pre>
* Destroys an entity.
* Sends out a ServerEntityDestroy packet to all players who know about the
* given entity.
*
* This only sends out the entity destroy packet and remove the entity from
* all knows about lists.
* It will not handle any EntityManager related cleanups.
* Only call this for example for player entities.
*
* If you want to immediately destroy an entity, use the entity's die()
* function which will remove it from every player knows about and send all
* needed updates.
* It will also them remove itself from the entity manager.
* </pre>
*
* @param entity
*/
public void destroyEntity(AEntity entity)
{
// Send destroy entity update
ServerEntityDestroy destroyPacket = new ServerEntityDestroy();
destroyPacket.entityId = entity.getEntityId();
destroyPacket.setCullPosition(new Vector3(entity.getPosition()));
this.addPacket(destroyPacket);
// Remove from knows about
for (int i = 0; i < this.clients.length; i++)
{
if (this.clients[i] != null)
this.clients[i].removeEntityFromKnowsAbout(entity);
}
}
/**
* Returns a free slot index in the clients array. Free means null. Returns
* -1 if there is no free slot.
*
* @return
*/
private int findFreeSlot()
{
// Iterate through all indices
synchronized (this.clientsLockObject)
{
for (int i = 0; i < this.clients.length; i++)
{
if (this.clients[i] == null)
return i;
}
}
return -1;
}
/**
* @return the isRunning
*/
public boolean isRunning()
{
return isRunning;
}
/**
* Prepares the save path folder structure.
* It will create all needed folders and files and initializes the files if they aren't already.
*/
private void prepareSaveStructure()
{
// Main folder
File f = new File(this.savePath);
if (!f.exists())
{
f.mkdir();
}
// Players folder
f = new File(this.savePath + "players/");
if (!f.exists())
{
f.mkdir();
}
}
/**
* The thread run function.
* Listens on the port given in the constructor.
* Will accept connections and add them to the clients[] socket.
*
* The socket update thread will handle all the game logic.
*/
@Override
public void run()
{
while (this.isRunning)
{
try
{
// Accept socket
Socket socket = this.serverSocket.accept();
if (socket == null)
{
ConsoleHelper.writeLog("error", "Got null socket!", "Server");
break;
}
else
ConsoleHelper.writeLog("info", "Got connection from " + socket.getRemoteSocketAddress(), "ServerSocket");
// Free slot?
int freeSlot = this.findFreeSlot();
if (freeSlot == -1)
{
ConsoleHelper.writeLog("info", "Connection from " + socket.getRemoteSocketAddress() + " dropped (no free slot)!", "ServerSocket");
socket.close();
continue;
}
// We got a free slot
ConsoleHelper.writeLog("info", "Connection from " + socket.getRemoteSocketAddress() + " accepted (slot " + freeSlot + ")!", "ServerSocket");
this.clients[freeSlot] = new CubicWorldServerClient(this, socket, freeSlot);
}
catch (IOException e)
{
ConsoleHelper.writeLog("error", "IOException in server main loop: " + e.getMessage(), "Server Main");
}
}
}
/**
* <pre>
* Spawns the entity with the given type id and sets its position to the
* given position.
* This function is only to simplify things.
* Spawning an entity works like this:
*
* 1. Retrieve free id from entity manager (entityManager.getNextFreeId())
* 2. Instantiate entity by new EntityClass() or
* EntitySystem.instantiateEntity(typeId)
* 3. Add the instance with the given id to the entitymanager by add().
*
* </pre>
*
* @param entityTypeId
* @param position
*/
public void spawnEntity(int entityTypeId, Vector3 position)
{
synchronized (this.entityManagerLockObject)
{
// Get new entity id
int entityId = this.entityManager.getNextFreeId();
// Instantiate entity
AEntity entity = EntitySystem.instantiateEntity(entityTypeId);
entity.setPosition(position);
// Add to entity manager
this.entityManager.add(entityId, entity);
}
}
/**
* Stops this server.
*/
public void stop()
{
// Terminate!
this.isRunning = false;
try
{
this.saveThread.join();
this.updateThread.join();
this.serverThread.join();
}
catch (Exception e)
{
// Ignore!
}
}
}