Implemented scalable snapshot sizes

This commit is contained in:
Anuken
2018-06-28 20:49:12 -04:00
parent fdf7173f14
commit ab82faf7ba
9 changed files with 124 additions and 11 deletions

View File

@@ -437,3 +437,6 @@ block.itemvoid.name=Item Void
block.liquidsource.name=Liquid Source block.liquidsource.name=Liquid Source
block.powervoid.name=Power Void block.powervoid.name=Power Void
block.powerinfinite.name=Power Infinite block.powerinfinite.name=Power Infinite
block.unloader.name=Unloader
block.sortedunloader.name=Sorted Unloader
block.vault.name=Vault

View File

@@ -12,9 +12,11 @@ import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.ItemDrop; import io.anuke.mindustry.entities.effect.ItemDrop;
import io.anuke.mindustry.entities.effect.Puddle; import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.entities.effect.Shield; import io.anuke.mindustry.entities.effect.Shield;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.units.BaseUnit; import io.anuke.mindustry.entities.units.BaseUnit;
import io.anuke.mindustry.game.Team; import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.io.Version; import io.anuke.mindustry.io.Version;
import io.anuke.mindustry.net.Net;
import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup; import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.trait.DrawTrait; import io.anuke.ucore.entities.trait.DrawTrait;
@@ -158,12 +160,20 @@ public class Vars{
fireGroup = Entities.addGroup(Fire.class, false).enableMapping(); fireGroup = Entities.addGroup(Fire.class, false).enableMapping();
unitGroups = new EntityGroup[Team.all.length]; unitGroups = new EntityGroup[Team.all.length];
threads = new ThreadHandler(Platform.instance.getThreadProvider());
for(Team team : Team.all){ for(Team team : Team.all){
unitGroups[team.ordinal()] = Entities.addGroup(BaseUnit.class).enableMapping(); unitGroups[team.ordinal()] = Entities.addGroup(BaseUnit.class).enableMapping();
} }
for(EntityGroup<?> group : Entities.getAllGroups()){
group.setRemoveListener(entity -> {
if(entity instanceof SyncTrait && Net.client()){
netClient.addRemovedEntity(((SyncTrait) entity).getID());
}
});
}
threads = new ThreadHandler(Platform.instance.getThreadProvider());
mobile = Gdx.app.getType() == ApplicationType.Android || Gdx.app.getType() == ApplicationType.iOS || testMobile; mobile = Gdx.app.getType() == ApplicationType.Android || Gdx.app.getType() == ApplicationType.iOS || testMobile;
ios = Gdx.app.getType() == ApplicationType.iOS; ios = Gdx.app.getType() == ApplicationType.iOS;
android = Gdx.app.getType() == ApplicationType.Android; android = Gdx.app.getType() == ApplicationType.Android;

View File

@@ -2,6 +2,7 @@ package io.anuke.mindustry.core;
import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Base64Coder; import com.badlogic.gdx.utils.Base64Coder;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.Pools; import com.badlogic.gdx.utils.Pools;
import io.anuke.annotations.Annotations.Remote; import io.anuke.annotations.Annotations.Remote;
import io.anuke.annotations.Annotations.Variant; import io.anuke.annotations.Annotations.Variant;
@@ -24,6 +25,7 @@ import io.anuke.ucore.io.ReusableByteArrayInputStream;
import io.anuke.ucore.io.delta.DEZDecoder; import io.anuke.ucore.io.delta.DEZDecoder;
import io.anuke.ucore.modules.Module; import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log; import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Timer; import io.anuke.ucore.util.Timer;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -47,10 +49,20 @@ public class NetClient extends Module {
private int lastSent; private int lastSent;
/**Last snapshot recieved.*/ /**Last snapshot recieved.*/
private byte[] lastSnapshot; private byte[] lastSnapshot;
/**Current snapshot that is being built from chinks.*/
private byte[] currentSnapshot;
/**Array of recieved chunk statuses.*/
private boolean[] recievedChunks;
/**Counter of how many chunks have been recieved.*/
private int recievedChunkCounter;
/**ID of snapshot that is currently being constructed.*/
private int currentSnapshotID;
/**Last snapshot ID recieved.*/ /**Last snapshot ID recieved.*/
private int lastSnapshotID = -1; private int lastSnapshotID = -1;
/**Decoder for uncompressing snapshots.*/ /**Decoder for uncompressing snapshots.*/
private DEZDecoder decoder = new DEZDecoder(); private DEZDecoder decoder = new DEZDecoder();
/**List of entities that were removed, and need not be added while syncing.*/
private IntSet removed = new IntSet();
/**Byte stream for reading in snapshots.*/ /**Byte stream for reading in snapshots.*/
private ReusableByteArrayInputStream byteStream = new ReusableByteArrayInputStream(); private ReusableByteArrayInputStream byteStream = new ReusableByteArrayInputStream();
private DataInputStream dataStream = new DataInputStream(byteStream); private DataInputStream dataStream = new DataInputStream(byteStream);
@@ -63,9 +75,15 @@ public class NetClient extends Module {
player.isAdmin = false; player.isAdmin = false;
Net.setClientLoaded(false); Net.setClientLoaded(false);
removed.clear();
timeoutTime = 0f; timeoutTime = 0f;
connecting = true; connecting = true;
quiet = false; quiet = false;
lastSent = 0;
lastSnapshot = null;
currentSnapshot = null;
currentSnapshotID = 0;
lastSnapshotID = -1;
ui.chatfrag.clearMessages(); ui.chatfrag.clearMessages();
ui.loadfrag.hide(); ui.loadfrag.hide();
@@ -160,6 +178,14 @@ public class NetClient extends Module {
Net.disconnect(); Net.disconnect();
} }
public synchronized void addRemovedEntity(int id){
removed.add(id);
}
public synchronized boolean isEntityUsed(int id){
return removed.contains(id);
}
void sync(){ void sync(){
if(timer.get(0, playerSyncTime)){ if(timer.get(0, playerSyncTime)){
@@ -213,13 +239,47 @@ public class NetClient extends Module {
} }
@Remote(variants = Variant.one, unreliable = true) @Remote(variants = Variant.one, unreliable = true)
public static void onSnapshot(byte[] snapshot, int snapshotID){ public static void onSnapshot(byte[] chunk, int snapshotID, short chunkID, short totalLength){
//skip snapshot IDs that have already been recieved //skip snapshot IDs that have already been recieved
if(snapshotID == netClient.lastSnapshotID){ if(snapshotID == netClient.lastSnapshotID){
return; return;
} }
try { try {
byte[] snapshot;
//total length exceeds that needed to hold one snapshot, therefore, it is split into chunks
if(totalLength > NetServer.maxSnapshotSize) {
//total amount of chunks to recieve
int totalChunks = Mathf.ceil((float) totalLength / NetServer.maxSnapshotSize);
//reset status when a new snapshot sending begins
if (netClient.currentSnapshotID != snapshotID) {
netClient.currentSnapshotID = snapshotID;
netClient.currentSnapshot = new byte[totalLength];
netClient.recievedChunkCounter = 0;
netClient.recievedChunks = new boolean[totalChunks];
}
//if this chunk hasn't been recieved yet...
if (!netClient.recievedChunks[chunkID]) {
netClient.recievedChunks[chunkID] = true;
netClient.recievedChunkCounter ++; //update recieved status
//copy the recieved bytes into the holding array
System.arraycopy(chunk, 0, netClient.currentSnapshot, chunkID * NetServer.maxSnapshotSize,
Math.min(NetServer.maxSnapshotSize, totalLength - chunkID * NetServer.maxSnapshotSize));
}
//when all chunks have been recieved, begin
if(netClient.recievedChunkCounter >= totalChunks){
snapshot = netClient.currentSnapshot;
}else{
return;
}
}else{
snapshot = chunk;
}
byte[] result; byte[] result;
int length; int length;
if (snapshotID == 0) { //fresh snapshot if (snapshotID == 0) { //fresh snapshot
@@ -236,7 +296,7 @@ public class NetClient extends Module {
netClient.lastSnapshotID = snapshotID; netClient.lastSnapshotID = snapshotID;
//set stream bytes to begin write //set stream bytes to begin snapshot reaeding
netClient.byteStream.setBytes(result, 0, length); netClient.byteStream.setBytes(result, 0, length);
//get data input for reading from the stream //get data input for reading from the stream
@@ -287,8 +347,9 @@ public class NetClient extends Module {
throw new RuntimeException("Error reading entity of type '"+ group.getType() + "': Read length mismatch [write=" + readLength + ", read=" + (netClient.byteStream.position() - position - 1)+ "]"); throw new RuntimeException("Error reading entity of type '"+ group.getType() + "': Read length mismatch [write=" + readLength + ", read=" + (netClient.byteStream.position() - position - 1)+ "]");
} }
if(add){ if(add && !netClient.isEntityUsed(entity.getID())){
entity.add(); entity.add();
netClient.addRemovedEntity(entity.getID());
} }
} }
} }

View File

@@ -34,10 +34,14 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import static io.anuke.mindustry.Vars.*; import static io.anuke.mindustry.Vars.*;
public class NetServer extends Module{ public class NetServer extends Module{
public final static int maxSnapshotSize = 2047;
private final static boolean showSnapshotSize = false;
private final static float serverSyncTime = 4, kickDuration = 30 * 1000; private final static float serverSyncTime = 4, kickDuration = 30 * 1000;
private final static Vector2 vector = new Vector2(); private final static Vector2 vector = new Vector2();
/**If a play goes away of their server-side coordinates by this distance, they get teleported back.*/ /**If a play goes away of their server-side coordinates by this distance, they get teleported back.*/
@@ -319,7 +323,7 @@ public class NetServer extends Module{
//if the player hasn't acknowledged that it has recieved the packet, send the same thing again //if the player hasn't acknowledged that it has recieved the packet, send the same thing again
if(connection.lastSentSnapshotID > connection.lastSnapshotID){ if(connection.lastSentSnapshotID > connection.lastSnapshotID){
Call.onSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID); sendSplitSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID);
return; return;
}else{ }else{
//set up last confirmed snapshot to the last one that was sent, otherwise //set up last confirmed snapshot to the last one that was sent, otherwise
@@ -394,13 +398,17 @@ public class NetServer extends Module{
byte[] bytes = syncStream.toByteArray(); byte[] bytes = syncStream.toByteArray();
connection.lastSentSnapshot = bytes; connection.lastSentSnapshot = bytes;
if(connection.lastSnapshotID == -1){ if(connection.lastSnapshotID == -1){
if(showSnapshotSize) Log.info("Sent raw snapshot: {0} bytes.", bytes.length);
//no snapshot to diff, send it all //no snapshot to diff, send it all
Call.onSnapshot(connection.id, bytes, 0); //Call.onSnapshot(connection.id, bytes, 0, 0);
sendSplitSnapshot(connection.id, bytes, 0);
connection.lastSnapshotID = 0; connection.lastSnapshotID = 0;
}else{ }else{
//send diff, otherwise //send diff, otherwise
byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.lastSnapshot, bytes), encoder); byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.lastSnapshot, bytes), encoder);
Call.onSnapshot(connection.id, diff, connection.lastSnapshotID + 1); if(showSnapshotSize) Log.info("Shrank snapshot: {0} -> {1}", bytes.length, diff.length);
//Call.onSnapshot(connection.id, diff, connection.lastSnapshotID + 1, 0);
sendSplitSnapshot(connection.id, diff, connection.lastSnapshotID + 1);
//increment snapshot ID //increment snapshot ID
connection.lastSentSnapshotID ++; connection.lastSentSnapshotID ++;
} }
@@ -411,6 +419,27 @@ public class NetServer extends Module{
} }
} }
/**Sends a raw byte[] snapshot to a client, splitting up into chunks when needed.*/
private static void sendSplitSnapshot(int userid, byte[] bytes, int snapshotID){
if(bytes.length < maxSnapshotSize){
Call.onSnapshot(userid, bytes, snapshotID, (short)0, (short)bytes.length);
}else{
int remaining = bytes.length;
int offset = 0;
int chunkid = 0;
while(remaining > 0){
int used = Math.min(remaining, maxSnapshotSize);
//TODO optimize to *not* copy the bytes directly, but instead re-use all arrays that are of length = maxSnapshotSize
byte[] toSend = Arrays.copyOfRange(bytes, offset, Math.min(offset + maxSnapshotSize, bytes.length));
Call.onSnapshot(userid, toSend, snapshotID, (short)chunkid, (short)bytes.length);
remaining -= used;
offset += used;
chunkid ++;
}
}
}
private static void onDisconnect(Player player){ private static void onDisconnect(Player player){
Call.sendMessage("[accent]" + player.name + " has disconnected."); Call.sendMessage("[accent]" + player.name + " has disconnected.");
Call.onPlayerDisconnect(player.id); Call.onPlayerDisconnect(player.id);

View File

@@ -1,14 +1,20 @@
package io.anuke.mindustry.entities.units; package io.anuke.mindustry.entities.units;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.Items; import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.gen.CallEntity; import io.anuke.mindustry.gen.CallEntity;
import io.anuke.mindustry.type.Item; import io.anuke.mindustry.type.Item;
import io.anuke.ucore.util.Mathf; import io.anuke.ucore.util.Mathf;
public class UnitDrops { public class UnitDrops {
private static final int maxItems = 200;
private static Item[] dropTable; private static Item[] dropTable;
public static void dropItems(BaseUnit unit){ public static void dropItems(BaseUnit unit){
if(Vars.itemGroup.size() > maxItems){
return;
}
if(dropTable == null){ if(dropTable == null){
dropTable = new Item[]{Items.tungsten, Items.lead, Items.carbide}; dropTable = new Item[]{Items.tungsten, Items.lead, Items.carbide};
} }

View File

@@ -8,6 +8,7 @@ import io.anuke.mindustry.game.UnlockableContent;
import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Block;
import io.anuke.ucore.util.Bundles; import io.anuke.ucore.util.Bundles;
import io.anuke.ucore.util.Log; import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Strings;
import java.util.Arrays; import java.util.Arrays;
@@ -59,7 +60,8 @@ public class Recipe implements UnlockableContent{
@Override @Override
public void init() { public void init() {
if(!Bundles.has("block." + result.name + ".name")) { if(!Bundles.has("block." + result.name + ".name")) {
Log.err("WARNING: Recipe block '{0}' does not have a formal name defined.", result.name); Log.err("WARNING: Recipe block '{0}' does not have a formal name defined. Add the following to bundle.properties:", result.name);
Log.err("block.{0}.name={1}", result.name, Strings.capitalize(result.name.replace('-', '_')));
}/*else if(result.fullDescription == null){ }/*else if(result.fullDescription == null){
Log.err("WARNING: Recipe block '{0}' does not have a description defined.", result.name); Log.err("WARNING: Recipe block '{0}' does not have a description defined.", result.name);
}*/ }*/

View File

@@ -132,6 +132,7 @@ public class Weapon extends Upgrade {
@Remote(targets = Loc.server, called = Loc.both, in = In.entities, unreliable = true) @Remote(targets = Loc.server, called = Loc.both, in = In.entities, unreliable = true)
public static void onPlayerShootWeapon(Player player, float x, float y, float rotation, boolean left){ public static void onPlayerShootWeapon(Player player, float x, float y, float rotation, boolean left){
if(player == null) return;
//clients do not see their own shoot events: they are simulated completely clientside to prevent laggy visuals //clients do not see their own shoot events: they are simulated completely clientside to prevent laggy visuals
//messing with the firerate or any other stats does not affect the server (take that, script kiddies!) //messing with the firerate or any other stats does not affect the server (take that, script kiddies!)
if(Net.client() && player == Vars.players[0]){ if(Net.client() && player == Vars.players[0]){
@@ -143,6 +144,7 @@ public class Weapon extends Upgrade {
@Remote(targets = Loc.server, called = Loc.both, in = In.entities, unreliable = true) @Remote(targets = Loc.server, called = Loc.both, in = In.entities, unreliable = true)
public static void onGenericShootWeapon(ShooterTrait shooter, float x, float y, float rotation, boolean left){ public static void onGenericShootWeapon(ShooterTrait shooter, float x, float y, float rotation, boolean left){
if(shooter == null) return;
shootDirect(shooter, x, y, rotation, left); shootDirect(shooter, x, y, rotation, left);
} }

View File

@@ -54,7 +54,7 @@ public class KryoClient implements ClientProvider{
} }
}; };
client = new Client(8192, 2048, connection -> new ByteSerializer()); client = new Client(8192, 4096, connection -> new ByteSerializer());
client.setDiscoveryHandler(handler); client.setDiscoveryHandler(handler);
Listener listener = new Listener(){ Listener listener = new Listener(){

View File

@@ -52,7 +52,7 @@ public class KryoServer implements ServerProvider {
int lastconnection = 0; int lastconnection = 0;
public KryoServer(){ public KryoServer(){
server = new Server(4096*2, 2048, connection -> new ByteSerializer()); server = new Server(4096*2, 4096, connection -> new ByteSerializer());
server.setDiscoveryHandler((datagramChannel, fromAddress) -> { server.setDiscoveryHandler((datagramChannel, fromAddress) -> {
ByteBuffer buffer = NetworkIO.writeServerData(); ByteBuffer buffer = NetworkIO.writeServerData();
buffer.position(0); buffer.position(0);