package net.kennux.cubicworld; import java.util.ArrayList; import net.kennux.cubicworld.entity.AEntity; import net.kennux.cubicworld.networking.CubicWorldServerClient; import net.kennux.cubicworld.networking.IPacketModel; import net.kennux.cubicworld.networking.packet.ServerEntitySpawn; import net.kennux.cubicworld.networking.packet.ServerEntityUpdate; import net.kennux.cubicworld.util.ConsoleHelper; import net.kennux.cubicworld.util.Mathf; import com.badlogic.gdx.math.Vector3; /** * Cubic world server update thread runnable. * * @author KennuX * */ public class CubicWorldServerUpdateThread implements Runnable { /** * The cubic world server instance. */ private CubicWorldServer server; /** * If a tick took more time than it should to compensate lag deltatime will get set to max time per tick and the milliseconds left are added to this long. * If a tick then took less time tan it could, this value will get decremented again. * * IMPORTANT: This is an experimental feature! */ private long millisBehind; public CubicWorldServerUpdateThread(CubicWorldServer server) { this.server = server; } /** * <pre> * Executes the updates in the rate given in ticksPerSecond. * Update execution flow: * * - Client entity updates * - Player socket update / cleanup * - World simulation * - World update * - World cleanup * - Packet sending * - Entity updating * - Day/night cycle * * </pre> * */ @Override public void run() { while (server.isRunning()) { long millis = System.currentTimeMillis(); server.profiler.startProfiling("Playersocket update / slot cleanup", ""); // Update player sockets synchronized (server.clientsLockObject) { for (int i = 0; i < server.clients.length; i++) { // Cleanup if (server.clients[i] != null && !server.clients[i].isAlive()) { ConsoleHelper.writeLog("error", "Dropped connection of socket in slot " + i, "Server"); if (server.clients[i].isLoggedin()) { this.server.destroyEntity(server.clients[i].playerEntity); } server.clients[i].close(); server.clients[i] = null; } else if (server.clients[i] != null) { server.clients[i].update(); } } } server.profiler.stopProfiling("Playersocket update / slot cleanup"); server.profiler.startProfiling("Server Client Entity Interpolation", ""); // Update all client entities (interpolates their position // instantly) for (CubicWorldServerClient client : server.clients) { if (client != null && client.isLoggedin()) client.playerEntity.interpolatePosition(true); } server.profiler.stopProfiling("Server Client Entity Interpolation"); server.profiler.startProfiling("Server WorldUpdate", ""); server.voxelWorld.update(); server.profiler.stopProfiling("Server WorldUpdate"); // Get player positions ArrayList<Vector3> playerPositions = new ArrayList<Vector3>(); for (CubicWorldServerClient player : server.clients) { if (player != null && player.isLoggedin()) { playerPositions.add(player.playerEntity.getPosition()); } } Vector3[] playerPositionsArray = playerPositions.toArray(new Vector3[playerPositions.size()]); server.profiler.startProfiling("Server World Cleanup", ""); // Cleanup server.voxelWorld.cleanup(playerPositionsArray, CubicWorldConfiguration.chunkLoadDistance); server.profiler.stopProfiling("Server World Cleanup"); // Send packets synchronized (server.packetStackLockObject) { server.profiler.startProfiling("Server Packet Sending", "Packetcount: " + server.packets.size()); while (!server.packets.isEmpty()) { IPacketModel packet = server.packets.pop(); // Send packet int playerId = packet.getPlayerId(); if (playerId == -1) { // Broadcast synchronized (server.clientsLockObject) { for (CubicWorldServerClient client : server.clients) { if (client != null && client.isLoggedin()) client.sendPacket(packet); } } } else if (playerId == -2) { // Distance-culled Broadcast synchronized (server.clientsLockObject) { for (CubicWorldServerClient client : server.clients) { // Client loggedin and in update distance? if (client != null && client.isLoggedin() && new Vector3(client.playerEntity.getPosition()).sub(packet.getCullPosition()).len() <= packet.getCullDistance()) { client.sendPacket(packet); } } } } else { // Single user packet. synchronized (server.clientsLockObject) { if (server.clients[packet.getPlayerId()] != null && server.clients[packet.getPlayerId()].isLoggedin()) server.clients[packet.getPlayerId()].sendPacket(packet); } } } server.profiler.stopProfiling("Server Packet Sending"); } // Entity update routine // First will update() all entities in the entitymanager // Then it will iterate thorough every client playerentity and // constructs the entity update or spawn packet based on the // knowsabout return value. // Then it will do the same thing for all entities in entity // manager. synchronized (server.clientsLockObject) { AEntity entities[] = null; // Get entity list synchronized (server.entityManagerLockObject) { this.server.entityManager.update(); entities = this.server.entityManager.getEntityArray(); } server.profiler.startProfiling("Server Entity Sync", ""); // For every client for (int clientId = 0; clientId < this.server.clients.length; clientId++) { CubicWorldServerClient client = this.server.clients[clientId]; // Only if client was loggedin if (client != null && client.isLoggedin()) { // Player updates for (int i = 0; i < server.clients.length; i++) { CubicWorldServerClient otherClient = server.clients[i]; // Already connected and loggedin? if (i != clientId && otherClient != null && otherClient.isLoggedin()) { boolean knowsAbout = client.checkIfPlayerKnowsAbout(otherClient.playerEntity); // Check if player knows about and in entity // view distancePerform distance culling if (knowsAbout || otherClient.playerEntity.isInEntityViewDistance(client.playerEntity)) { if (knowsAbout) { // Add to update entity list ServerEntityUpdate entityUpdate = new ServerEntityUpdate(); entityUpdate.entity = otherClient.playerEntity; entityUpdate.setPlayerId(clientId); this.server.addPacket(entityUpdate); } else { // Send spawn packet ServerEntitySpawn spawnPacket = new ServerEntitySpawn(); // Init spawn packet spawnPacket.entity = otherClient.playerEntity; spawnPacket.setPlayerId(clientId); // Knows about now client.addEntityToKnowsAbout(otherClient.playerEntity); this.server.addPacket(spawnPacket); } } else { client.removeEntityFromKnowsAbout(otherClient.playerEntity); } } } // Entity updates if (entities != null) for (int i = 0; i < entities.length; i++) { AEntity entity = entities[i]; // Check if player knows about and in entity // view distance. Perform distance culling if (entity != null) { boolean knowsAbout = client.checkIfPlayerKnowsAbout(entity); if (knowsAbout || entity.isInEntityViewDistance(client.playerEntity)) { if (knowsAbout) { // Add to update entity list ServerEntityUpdate entityUpdate = new ServerEntityUpdate(); entityUpdate.entity = entity; entityUpdate.setPlayerId(clientId); this.server.addPacket(entityUpdate); } else { // Send spawn packet ServerEntitySpawn spawnPacket = new ServerEntitySpawn(); // Init spawn packet spawnPacket.entity = entity; spawnPacket.setPlayerId(clientId); // Knows about now client.addEntityToKnowsAbout(entity); this.server.addPacket(spawnPacket); } } } else { client.removeEntityFromKnowsAbout(entity); } } } } server.profiler.stopProfiling("Server Entity Sync"); } server.profiler.startProfiling("Server Daynight Cycle", ""); // Update time this.server.dayNightCycle.tick(); server.profiler.stopProfiling("Server Daynight Cycle"); // Update plugins this.server.profiler.startProfiling("Plugin updates", ""); this.server.pluginManager.fireEvent("update", true); this.server.profiler.stopProfiling("Plugin updates"); server.profiler.startProfiling("Entity Cleanup", ""); // Entity cleanup this.server.entityManager.cleanup(playerPositionsArray); server.profiler.stopProfiling("Entity Cleanup"); // Write update packet. this.server.addPacket(this.server.dayNightCycle.getTimeUpdatePacket()); this.server.tick++; // Delta time and lag compensation calculation // Lag compensation works like this: // If a lag occurs (deltaTime bigger than it should be), the milliseconds the server is behind gets counted. // Then if for example a tick only takes 1 ms while 200ms behind, the deltatime will be 50 and the server then is just 150ms behind. int deltaTime = (int) (System.currentTimeMillis() - millis); int millisPerTick = (1000 / server.ticksPerSecond); int spareTime = millisPerTick - deltaTime; // Delta time more than it should be? if (deltaTime > millisPerTick) { // Server is hanging behind :/ this.millisBehind = deltaTime - millisPerTick; deltaTime = millisPerTick; } // Or less than it could be and is millis behind? else if (deltaTime < millisPerTick && this.millisBehind > 0) { this.millisBehind -= spareTime; if (this.millisBehind < 0) this.millisBehind = 0; deltaTime = deltaTime + (int) Mathf.min(spareTime, this.millisBehind); } // ConsoleHelper.writeLog("info", "Delta time: " + deltaTime + ", Millis behind: " + this.millisBehind, "Server"); try { if (spareTime > 0) Thread.sleep(spareTime); /* * else * ConsoleHelper.writeLog("info", "Can't keep up! Delta time: " + deltaTime + ", Millis behind: " + this.millisBehind, "Server"); */ } catch (InterruptedException e) { } this.server.deltaTime = deltaTime; server.profiler.reset(); } } }