From b4151a256b6c90fdb1acb4b536dfaca676a11171 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 29 Jun 2018 14:13:31 -0400 Subject: [PATCH] Extensive netcode changes, bugfixes --- build.gradle | 2 +- .../io/anuke/mindustry/core/NetClient.java | 45 +++++--- .../io/anuke/mindustry/core/NetServer.java | 72 ++++++------ .../io/anuke/mindustry/entities/Player.java | 5 - .../anuke/mindustry/input/DesktopInput.java | 7 +- .../anuke/mindustry/input/InputHandler.java | 6 +- .../io/anuke/mindustry/net/NetConnection.java | 23 ++-- .../src/io/anuke/mindustry/net/NetworkIO.java | 103 +++++++++++------- core/src/io/anuke/mindustry/net/Packets.java | 4 +- .../mindustry/ui/dialogs/JoinDialog.java | 9 +- .../ui/dialogs/SettingsMenuDialog.java | 2 +- .../ui/fragments/LoadingFragment.java | 15 +++ core/src/io/anuke/mindustry/world/Build.java | 22 +++- .../mindustry/world/blocks/BreakBlock.java | 7 +- .../anuke/mindustry/desktop/CrashHandler.java | 24 +++- .../src/io/anuke/kryonet/CustomListeners.java | 89 +++++++++++++++ kryonet/src/io/anuke/kryonet/KryoClient.java | 14 +-- kryonet/src/io/anuke/kryonet/KryoCore.java | 89 +++++++++++++++ .../src/io/anuke/kryonet/KryoRegistrator.java | 45 -------- kryonet/src/io/anuke/kryonet/KryoServer.java | 15 ++- 20 files changed, 411 insertions(+), 187 deletions(-) create mode 100644 kryonet/src/io/anuke/kryonet/CustomListeners.java create mode 100644 kryonet/src/io/anuke/kryonet/KryoCore.java delete mode 100644 kryonet/src/io/anuke/kryonet/KryoRegistrator.java diff --git a/build.gradle b/build.gradle index 284eae948f..c5f1401aa9 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ allprojects { gdxVersion = '1.9.8' roboVMVersion = '2.3.0' aiVersion = '1.8.1' - uCoreVersion = '5d685fc8b1' + uCoreVersion = '2a575ccc77' getVersionString = { String buildVersion = getBuildVersion() diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index b45d23d5fc..0592e34a50 100644 --- a/core/src/io/anuke/mindustry/core/NetClient.java +++ b/core/src/io/anuke/mindustry/core/NetClient.java @@ -47,8 +47,11 @@ public class NetClient extends Module { private float timeoutTime = 0f; /**Last sent client snapshot ID.*/ private int lastSent; + + /**Last snapshot ID recieved.*/ + private int lastSnapshotBaseID = -1; /**Last snapshot recieved.*/ - private byte[] lastSnapshot; + private byte[] lastSnapshotBase; /**Current snapshot that is being built from chinks.*/ private byte[] currentSnapshot; /**Array of recieved chunk statuses.*/ @@ -57,8 +60,7 @@ public class NetClient extends Module { private int recievedChunkCounter; /**ID of snapshot that is currently being constructed.*/ private int currentSnapshotID = -1; - /**Last snapshot ID recieved.*/ - private int lastSnapshotID = -1; + /**Decoder for uncompressing snapshots.*/ private DEZDecoder decoder = new DEZDecoder(); /**List of entities that were removed, and need not be added while syncing.*/ @@ -80,15 +82,22 @@ public class NetClient extends Module { connecting = true; quiet = false; lastSent = 0; - lastSnapshot = null; + lastSnapshotBase = null; currentSnapshot = null; currentSnapshotID = -1; - lastSnapshotID = -1; + lastSnapshotBaseID = -1; ui.chatfrag.clearMessages(); ui.loadfrag.hide(); ui.loadfrag.show("$text.connecting.data"); + ui.loadfrag.setButton(() -> { + ui.loadfrag.hide(); + connecting = false; + quiet = true; + Net.disconnect(); + }); + Entities.clear(); ConnectPacket c = new ConnectPacket(); @@ -191,7 +200,7 @@ public class NetClient extends Module { if(timer.get(0, playerSyncTime)){ ClientSnapshotPacket packet = Pools.obtain(ClientSnapshotPacket.class); - packet.lastSnapshot = lastSnapshotID; + packet.lastSnapshot = lastSnapshotBaseID; packet.snapid = lastSent++; Net.send(packet, SendMode.udp); } @@ -239,9 +248,12 @@ public class NetClient extends Module { } @Remote(variants = Variant.one, unreliable = true) - public static void onSnapshot(byte[] chunk, int snapshotID, short chunkID, short totalLength){ - //skip snapshot IDs that have already been recieved - if(snapshotID == netClient.lastSnapshotID){ + public static void onSnapshot(byte[] chunk, int snapshotID, short chunkID, short totalLength, int base){ + if(NetServer.showSnapshotSize) Log.info("Recieved snapshot: len {0} ID {1} chunkID {2} totalLength {3} base {4} client-base {5}", chunk.length, snapshotID, chunkID, totalLength, base, netClient.lastSnapshotBaseID); + + //skip snapshot IDs that have already been recieved OR snapshots that are too far in front + if(snapshotID < netClient.lastSnapshotBaseID || base != netClient.lastSnapshotBaseID){ + if(NetServer.showSnapshotSize) Log.info("//SKIP SNAPSHOT"); return; } @@ -280,21 +292,24 @@ public class NetClient extends Module { snapshot = chunk; } + if(NetServer.showSnapshotSize) Log.info("Finished recieving snapshot ID {0} length {1}", snapshotID, chunk.length); + byte[] result; int length; - if (snapshotID == 0) { //fresh snapshot + if (base == -1) { //fresh snapshot result = snapshot; length = snapshot.length; - netClient.lastSnapshot = Arrays.copyOf(snapshot, snapshot.length); + netClient.lastSnapshotBase = Arrays.copyOf(snapshot, snapshot.length); } else { //otherwise, last snapshot must not be null, decode it - netClient.decoder.init(netClient.lastSnapshot, snapshot); + if(NetServer.showSnapshotSize) Log.info("Base size: {0} Path size: {1}", netClient.lastSnapshotBase.length, snapshot.length); + netClient.decoder.init(netClient.lastSnapshotBase, snapshot); result = netClient.decoder.decode(); length = netClient.decoder.getDecodedLength(); //set last snapshot to a copy to prevent issues - netClient.lastSnapshot = Arrays.copyOf(result, length); + netClient.lastSnapshotBase = Arrays.copyOf(result, length); } - netClient.lastSnapshotID = snapshotID; + netClient.lastSnapshotBaseID = snapshotID; //set stream bytes to begin snapshot reaeding netClient.byteStream.setBytes(result, 0, length); @@ -355,7 +370,7 @@ public class NetClient extends Module { } //confirm that snapshot has been recieved - netClient.lastSnapshotID = snapshotID; + netClient.lastSnapshotBaseID = snapshotID; }catch (Exception e){ throw new RuntimeException(e); diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index a25d12f8e8..d15f317f57 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -40,9 +40,9 @@ import static io.anuke.mindustry.Vars.*; public class NetServer extends Module{ public final static int maxSnapshotSize = 2047; + public final static boolean showSnapshotSize = false; private final static byte[] reusableSnapArray = new byte[maxSnapshotSize]; - private final static boolean showSnapshotSize = false; private final static float serverSyncTime = 4, kickDuration = 30 * 1000; private final static Vector2 vector = new Vector2(); /**If a play goes away of their server-side coordinates by this distance, they get teleported back.*/ @@ -167,15 +167,15 @@ public class NetServer extends Module{ Net.handleServer(ClientSnapshotPacket.class, (id, packet) -> { Player player = connections.get(id); NetConnection connection = Net.getConnection(id); - if(player == null || connection == null || packet.snapid < connection.lastRecievedSnapshot) return; + if(player == null || connection == null || packet.snapid < connection.lastRecievedClientSnapshot) return; boolean verifyPosition = !player.isDead() && !debug && headless; - if(connection.lastRecievedTime == 0) connection.lastRecievedTime = TimeUtils.millis() - 16; + if(connection.lastRecievedClientTime == 0) connection.lastRecievedClientTime = TimeUtils.millis() - 16; - long elapsed = TimeUtils.timeSinceMillis(connection.lastRecievedTime); + long elapsed = TimeUtils.timeSinceMillis(connection.lastRecievedClientTime); - float maxSpeed = packet.boosting && !player.mech.flying ? player.mech.boostSpeed*3f : player.mech.speed*3f; + float maxSpeed = (packet.boosting && !player.mech.flying ? player.mech.boostSpeed : player.mech.speed)*2.5f; //extra 1.1x multiplicaton is added just in case float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.1f; @@ -184,6 +184,7 @@ public class NetServer extends Module{ player.pointerY = packet.pointerY; player.setMineTile(packet.mining); player.isBoosting = packet.boosting; + player.isShooting = packet.shooting; vector.set(packet.x - player.getInterpolator().target.x, packet.y - player.getInterpolator().target.y); @@ -210,12 +211,21 @@ public class NetServer extends Module{ player.getInterpolator().read(player.x, player.y, newx, newy, packet.timeSent, packet.rotation, packet.baseRotation); player.getVelocity().set(packet.xv, packet.yv); //only for visual calculation purposes, doesn't actually update the player - connection.lastSnapshotID = packet.lastSnapshot; - connection.lastRecievedSnapshot = packet.snapid; - connection.lastRecievedTime = TimeUtils.millis(); + //when the client confirms recieveing a snapshot, update base and clear map + if(packet.lastSnapshot > connection.currentBaseID){ + connection.currentBaseID = packet.lastSnapshot; + connection.currentBaseSnapshot = connection.lastSentRawSnapshot; + } + + connection.lastRecievedClientSnapshot = packet.snapid; + connection.lastRecievedClientTime = TimeUtils.millis(); }); - Net.handleServer(InvokePacket.class, (id, packet) -> RemoteReadServer.readPacket(packet.writeBuffer, packet.type, connections.get(id))); + Net.handleServer(InvokePacket.class, (id, packet) -> { + Player player = connections.get(id); + if(player == null) return; + RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player); + }); } public void update(){ @@ -314,7 +324,7 @@ public class NetServer extends Module{ for (Player player : connections.values()) { NetConnection connection = Net.getConnection(player.clientid); - if(connection == null){ + if(connection == null || !connection.isConnected()){ //player disconnected, ignore them onDisconnect(player); return; @@ -323,18 +333,16 @@ public class NetServer extends Module{ if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue; //if the player hasn't acknowledged that it has recieved the packet, send the same thing again - if(connection.lastSentSnapshotID > connection.lastSnapshotID){ - sendSplitSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID); + if(connection.currentBaseID < connection.lastSentSnapshotID){ + if(showSnapshotSize) Log.info("Re-sending snapshot: {0} bytes, ID {1} base {2} baselength {3}", connection.lastSentSnapshot.length, connection.lastSentSnapshotID, connection.lastSentBase, connection.currentBaseSnapshot.length); + sendSplitSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID, connection.lastSentBase); return; - }else{ - //set up last confirmed snapshot to the last one that was sent, otherwise - connection.lastSnapshot = connection.lastSentSnapshot; } //reset stream to begin writing syncStream.reset(); - //write wave data + //write wave datas dataStream.writeFloat(state.wavetime); dataStream.writeInt(state.wave); @@ -397,21 +405,22 @@ public class NetServer extends Module{ } byte[] bytes = syncStream.toByteArray(); - connection.lastSentSnapshot = bytes; - if(connection.lastSnapshotID == -1){ + + connection.lastSentRawSnapshot = bytes; + + if(connection.currentBaseID == -1){ if(showSnapshotSize) Log.info("Sent raw snapshot: {0} bytes.", bytes.length); - //no snapshot to diff, send it all - //Call.onSnapshot(connection.id, bytes, 0, 0); - sendSplitSnapshot(connection.id, bytes, 0); - connection.lastSnapshotID = 0; + ///Nothing to diff off of in this case, send the whole thing, but increment the counter + connection.lastSentSnapshot = bytes; + sendSplitSnapshot(connection.id, bytes, 0, -1); }else{ //send diff, otherwise - byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.lastSnapshot, bytes), encoder); - 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 - connection.lastSentSnapshotID ++; + byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.currentBaseSnapshot, bytes), encoder); + if(showSnapshotSize) Log.info("Shrank snapshot: {0} -> {1}, Base {2} ID {3}", bytes.length, diff.length, connection.currentBaseID, connection.lastSentSnapshotID); + sendSplitSnapshot(connection.id, diff, connection.lastSentSnapshotID + 1, connection.currentBaseID); + connection.lastSentSnapshot = diff; + connection.lastSentSnapshotID = connection.currentBaseID + 1; + connection.lastSentBase = connection.currentBaseID; } } @@ -421,9 +430,10 @@ 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){ + private static void sendSplitSnapshot(int userid, byte[] bytes, int snapshotID, int base){ if(bytes.length < maxSnapshotSize){ - Call.onSnapshot(userid, bytes, snapshotID, (short)0, (short)bytes.length); + if(showSnapshotSize) Log.info("Raw send() snapshot call: {0} bytes, sID {1}", bytes.length, snapshotID); + Call.onSnapshot(userid, bytes, snapshotID, (short)0, (short)bytes.length, base); }else{ int remaining = bytes.length; int offset = 0; @@ -438,7 +448,7 @@ public class NetServer extends Module{ }else { toSend = Arrays.copyOfRange(bytes, offset, Math.min(offset + maxSnapshotSize, bytes.length)); } - Call.onSnapshot(userid, toSend, snapshotID, (short)chunkid, (short)bytes.length); + Call.onSnapshot(userid, toSend, snapshotID, (short)chunkid, (short)bytes.length, base); remaining -= used; offset += used; diff --git a/core/src/io/anuke/mindustry/entities/Player.java b/core/src/io/anuke/mindustry/entities/Player.java index ab8f8605f6..767c5a1cc0 100644 --- a/core/src/io/anuke/mindustry/entities/Player.java +++ b/core/src/io/anuke/mindustry/entities/Player.java @@ -590,7 +590,6 @@ public class Player extends Unit implements BuilderTrait, CarryTrait, ShooterTra //autofire: mobile only! if(mobile) { - boolean lastShooting = isShooting; if (target == null) { isShooting = false; @@ -609,10 +608,6 @@ public class Player extends Unit implements BuilderTrait, CarryTrait, ShooterTra isShooting = true; } - //update status of shooting to server - if(lastShooting != isShooting){ - CallEntity.setShooting(isShooting); - } }else if(isShooting()){ Vector2 vec = Graphics.world(Vars.control.input(playerIndex).getMouseX(), Vars.control.input(playerIndex).getMouseY()); diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index 9a86be92cd..2e4f2f0060 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -6,7 +6,6 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion; import io.anuke.mindustry.content.blocks.Blocks; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.entities.Player; -import io.anuke.mindustry.gen.CallEntity; import io.anuke.mindustry.graphics.Palette; import io.anuke.mindustry.input.PlaceUtils.NormalizeDrawResult; import io.anuke.mindustry.input.PlaceUtils.NormalizeResult; @@ -140,7 +139,7 @@ public class DesktopInput extends InputHandler{ } if(player.isShooting && !canShoot()){ - CallEntity.setShooting(false); + player.isShooting = false; } if(isPlacing()){ @@ -207,7 +206,7 @@ public class DesktopInput extends InputHandler{ //only begin shooting if there's no cursor event if(!tileTapped(cursor) && !tryTapPlayer(worldx, worldy) && player.getPlaceQueue().size == 0 && !droppingItem && !tryBeginMine(cursor) && player.getMineTile() == null){ - CallEntity.setShooting(true); + player.isShooting = true; } } }else if(button == Buttons.RIGHT){ //right = begin breaking @@ -229,7 +228,7 @@ public class DesktopInput extends InputHandler{ @Override public boolean touchUp (int screenX, int screenY, int pointer, int button) { if(button == Buttons.LEFT){ - CallEntity.setShooting(false); + player.isShooting = false; } if(player.isDead() || state.is(State.menu) || ui.hasDialog()) return false; diff --git a/core/src/io/anuke/mindustry/input/InputHandler.java b/core/src/io/anuke/mindustry/input/InputHandler.java index 33a6bb331b..ef3c00e2ad 100644 --- a/core/src/io/anuke/mindustry/input/InputHandler.java +++ b/core/src/io/anuke/mindustry/input/InputHandler.java @@ -265,11 +265,6 @@ public abstract class InputHandler extends InputAdapter{ player.addBuildRequest(new BuildRequest(tile.x, tile.y)); } - @Remote(targets = Loc.client, called = Loc.both, in = In.entities) - public static void setShooting(Player player, boolean on){ - player.isShooting = on; - } - @Remote(targets = Loc.both, called = Loc.server, in = In.entities) public static void dropItem(Player player, float angle){ if(Net.server() && !player.inventory.hasItem()){ @@ -332,6 +327,7 @@ public abstract class InputHandler extends InputAdapter{ @Remote(targets = Loc.both, called = Loc.server, forward = true, in = In.blocks) public static void onTileTapped(Player player, Tile tile){ + if(tile == null || player == null) return; tile.block().tapped(tile, player); } diff --git a/core/src/io/anuke/mindustry/net/NetConnection.java b/core/src/io/anuke/mindustry/net/NetConnection.java index 06688402f9..b4b25a76be 100644 --- a/core/src/io/anuke/mindustry/net/NetConnection.java +++ b/core/src/io/anuke/mindustry/net/NetConnection.java @@ -6,20 +6,21 @@ public abstract class NetConnection { public final int id; public final String address; - /**ID of last snapshot this connection is guaranteed to have recieved.*/ - public int lastSnapshotID = -1; - /**Byte array of last sent snapshot data that is confirmed to be recieved.*/ - public byte[] lastSnapshot; + /**The current base snapshot that the client is absolutely confirmed to have recieved. + * All sent snapshots should be taking the diff from this base snapshot, if it isn't null.*/ + public byte[] currentBaseSnapshot; + /**ID of the current base snapshot.*/ + public int currentBaseID = -1; - /**ID of last sent snapshot.*/ - public int lastSentSnapshotID = -1; - /**Byte array of last sent snapshot.*/ + public int lastSentBase = -1; public byte[] lastSentSnapshot; + public byte[] lastSentRawSnapshot; + public int lastSentSnapshotID = -1; /**ID of last recieved client snapshot.*/ - public int lastRecievedSnapshot = -1; + public int lastRecievedClientSnapshot = -1; /**Timestamp of last recieved snapshot.*/ - public long lastRecievedTime; + public long lastRecievedClientTime; public boolean hasConnected = false; @@ -28,6 +29,10 @@ public abstract class NetConnection { this.address = address; } + public boolean isConnected(){ + return true; + } + public abstract void send(Object object, SendMode mode); public abstract void close(); } diff --git a/core/src/io/anuke/mindustry/net/NetworkIO.java b/core/src/io/anuke/mindustry/net/NetworkIO.java index bd1594253f..035decf9e3 100644 --- a/core/src/io/anuke/mindustry/net/NetworkIO.java +++ b/core/src/io/anuke/mindustry/net/NetworkIO.java @@ -59,29 +59,39 @@ public class NetworkIO { stream.writeShort(world.width()); stream.writeShort(world.height()); - for(int x = 0; x < world.width(); x ++){ - for(int y = 0; y < world.height(); y ++){ - Tile tile = world.tile(x, y); + for (int i = 0; i < world.width() * world.height(); i++) { + Tile tile = world.tile(i); - stream.writeByte(tile.floor().id); //floor ID - stream.writeByte(tile.block().id); //block ID - stream.writeByte(tile.elevation); + stream.writeByte(tile.getFloorID()); + stream.writeByte(tile.getWallID()); + stream.writeByte(tile.elevation); - if(tile.block() instanceof BlockPart){ - stream.writeByte(tile.link); - } + if(tile.block() instanceof BlockPart){ + stream.writeByte(tile.link); + }else if(tile.entity != null){ + stream.writeByte(Bits.packByte(tile.getTeamID(), tile.getRotation())); //team + rotation + stream.writeShort((short)tile.entity.health); //health - if(tile.entity != null){ - stream.writeByte(Bits.packByte((byte)tile.getTeam().ordinal(), tile.getRotation())); - stream.writeShort((short)tile.entity.health); //health + if(tile.entity.items != null) tile.entity.items.write(stream); + if(tile.entity.power != null) tile.entity.power.write(stream); + if(tile.entity.liquids != null) tile.entity.liquids.write(stream); - if(tile.entity.items != null) tile.entity.items.write(stream); - if(tile.entity.power != null) tile.entity.power.write(stream); - if(tile.entity.liquids != null) tile.entity.liquids.write(stream); + tile.entity.write(stream); + }else if(tile.getWallID() == 0){ + int consecutives = 0; - tile.entity.write(stream); + for (int j = i + 1; j < world.width() * world.height() && consecutives < 255; j++) { + Tile nextTile = world.tile(j); + + if(nextTile.getFloorID() != tile.getFloorID() || nextTile.getWallID() != 0 || nextTile.elevation != tile.elevation){ + break; + } + + consecutives ++; } + stream.writeByte(consecutives); + i += consecutives; } } @@ -158,38 +168,47 @@ public class NetworkIO { Tile[][] tiles = world.createTiles(width, height); - for(int x = 0; x < width; x ++){ - for(int y = 0; y < height; y ++){ - byte floorid = stream.readByte(); - byte blockid = stream.readByte(); - byte elevation = stream.readByte(); + for (int i = 0; i < width * height; i++) { + int x = i % width, y = i /width; + byte floorid = stream.readByte(); + byte wallid = stream.readByte(); + byte elevation = stream.readByte(); - Tile tile = new Tile(x, y, floorid, blockid); + Tile tile = new Tile(x, y, floorid, wallid); + tile.elevation = elevation; - tile.elevation = elevation; + if (wallid == Blocks.blockpart.id) { + tile.link = stream.readByte(); + }else if (tile.entity != null) { + byte tr = stream.readByte(); + short health = stream.readShort(); - if(tile.block() == Blocks.blockpart){ - tile.link = stream.readByte(); + byte team = Bits.getLeftByte(tr); + byte rotation = Bits.getRightByte(tr); + + tile.setTeam(Team.all[team]); + tile.entity.health = health; + tile.setRotation(rotation); + + if (tile.entity.items != null) tile.entity.items.read(stream); + if (tile.entity.power != null) tile.entity.power.read(stream); + if (tile.entity.liquids != null) tile.entity.liquids.read(stream); + + tile.entity.read(stream); + }else if(wallid == 0){ + int consecutives = stream.readUnsignedByte(); + + for (int j = i + 1; j < i + 1 + consecutives; j++) { + int newx = j % width, newy = j / width; + Tile newTile = new Tile(newx, newy, floorid, wallid); + newTile.elevation = elevation; + tiles[newx][newy] = newTile; } - if(tile.entity != null) { - byte tr = stream.readByte(); - short health = stream.readShort(); - - tile.setTeam(Team.all[Bits.getLeftByte(tr)]); - tile.setRotation(Bits.getRightByte(tr)); - - tile.entity.health = health; - - if (tile.entity.items != null) tile.entity.items.read(stream); - if (tile.entity.power != null) tile.entity.power.read(stream); - if (tile.entity.liquids != null) tile.entity.liquids.read(stream); - - tile.entity.read(stream); - } - - tiles[x][y] = tile; + i += consecutives; } + + tiles[x][y] = tile; } player.reset(); diff --git a/core/src/io/anuke/mindustry/net/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index 118a48788a..e627e3a19b 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -108,7 +108,7 @@ public class Packets { //player snapshot data public float x, y, pointerX, pointerY, rotation, baseRotation, xv, yv; public Tile mining; - public boolean boosting; + public boolean boosting, shooting; @Override public void write(ByteBuffer buffer) { @@ -123,6 +123,7 @@ public class Packets { buffer.putFloat(player.pointerX); buffer.putFloat(player.pointerY); buffer.put(player.isBoosting ? (byte)1 : 0); + buffer.put(player.isShooting ? (byte)1 : 0); buffer.put((byte)(Mathf.clamp(player.getVelocity().x, -Unit.maxAbsVelocity, Unit.maxAbsVelocity) * Unit.velocityPercision)); buffer.put((byte)(Mathf.clamp(player.getVelocity().y, -Unit.maxAbsVelocity, Unit.maxAbsVelocity) * Unit.velocityPercision)); @@ -144,6 +145,7 @@ public class Packets { pointerX = buffer.getFloat(); pointerY = buffer.getFloat(); boosting = buffer.get() == 1; + shooting = buffer.get() == 1; xv = buffer.get() / Unit.velocityPercision; yv = buffer.get() / Unit.velocityPercision; rotation = buffer.getShort()/2f; diff --git a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java index 34aa7f5728..4c6f9077f3 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java @@ -22,9 +22,7 @@ import io.anuke.ucore.util.Bundles; import io.anuke.ucore.util.Log; import io.anuke.ucore.util.Strings; -import static io.anuke.mindustry.Vars.maxNameLength; -import static io.anuke.mindustry.Vars.players; -import static io.anuke.mindustry.Vars.ui; +import static io.anuke.mindustry.Vars.*; public class JoinDialog extends FloatingDialog { Array servers = new Array<>(); @@ -275,6 +273,11 @@ public class JoinDialog extends FloatingDialog { void connect(String ip, int port){ ui.loadfrag.show("$text.connecting"); + ui.loadfrag.setButton(() -> { + ui.loadfrag.hide(); + netClient.disconnectQuietly(); + }); + Timers.runTask(2f, () -> { try{ Vars.netClient.beginConnecting(); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java index 6e4131a211..6d4eef6890 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java @@ -135,7 +135,7 @@ public class SettingsMenuDialog extends SettingsDialog{ game.sliderPref("saveinterval", 90, 10, 5*120, i -> Bundles.format("setting.seconds", i)); if(!gwt){ - graphics.checkPref("multithread", false, threads::setEnabled); + graphics.checkPref("multithread", true, threads::setEnabled); if(Settings.getBool("multithread")){ threads.setEnabled(true); diff --git a/core/src/io/anuke/mindustry/ui/fragments/LoadingFragment.java b/core/src/io/anuke/mindustry/ui/fragments/LoadingFragment.java index f663e406df..7dca4ca920 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/LoadingFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/LoadingFragment.java @@ -1,15 +1,18 @@ package io.anuke.mindustry.ui.fragments; import io.anuke.mindustry.graphics.Palette; +import io.anuke.ucore.function.Listenable; import io.anuke.ucore.scene.Group; import io.anuke.ucore.scene.builders.label; import io.anuke.ucore.scene.builders.table; import io.anuke.ucore.scene.event.Touchable; import io.anuke.ucore.scene.ui.Label; +import io.anuke.ucore.scene.ui.TextButton; import io.anuke.ucore.scene.ui.layout.Table; public class LoadingFragment implements Fragment { private Table table; + private TextButton button; @Override public void build(Group parent) { @@ -25,11 +28,22 @@ public class LoadingFragment implements Fragment { row(); get().addImage("white").growX() .height(3f).pad(4f).growX().get().setColor(Palette.accent); + + row(); + + button = get().addButton("$text.cancel", () -> {}).pad(20).size(250f, 70f).get(); + button.setVisible(false); }}.end().get(); table.setVisible(false); } + public void setButton(Listenable listener){ + button.setVisible(true); + button.getListeners().removeIndex(button.getListeners().size - 1); + button.clicked(listener); + } + public void show(){ show("$text.loading"); } @@ -42,5 +56,6 @@ public class LoadingFragment implements Fragment { public void hide(){ table.setVisible(false); + button.setVisible(false); } } diff --git a/core/src/io/anuke/mindustry/world/Build.java b/core/src/io/anuke/mindustry/world/Build.java index d3bb953e9e..fb7cc574a5 100644 --- a/core/src/io/anuke/mindustry/world/Build.java +++ b/core/src/io/anuke/mindustry/world/Build.java @@ -28,8 +28,12 @@ public class Build { /**Returns block type that was broken, or null if unsuccesful.*/ @Remote(targets = Loc.both, forward = true, called = Loc.server, in = In.blocks) public static void breakBlock(Player player, Team team, int x, int y){ - if(Net.server() && !validBreak(team, x, y)){ - return; + if(Net.server()){ + if(!validBreak(team, x, y)){ + return; + } + + team = player.getTeam(); //throw new ValidateException(player, "An invalid block has been broken."); } @@ -43,7 +47,7 @@ public class Build { Block previous = tile.block(); //remote players only - if(!player.isLocal){ + if(player != null && !player.isLocal){ player.getPlaceQueue().clear(); player.getPlaceQueue().addFirst(new BuildRequest(x, y)); } @@ -88,8 +92,12 @@ public class Build { /**Places a BuildBlock at this location. Call validPlace first.*/ @Remote(targets = Loc.both, forward = true, called = Loc.server, in = In.blocks) public static void placeBlock(Player player, Team team, int x, int y, Recipe recipe, int rotation){ - if(Net.server() && !validPlace(team, x, y, recipe.result, rotation)){ - return; + if(Net.server()){ + if(!validPlace(team, x, y, recipe.result, rotation)){ + return; + } + + team = player.getTeam(); //throw new ValidateException(player, "An invalid block has been placed."); } @@ -143,7 +151,9 @@ public class Build { } } - threads.runDelay(() -> Events.fire(BlockBuildEvent.class, team, tile)); + Team fteam = team; + + threads.runDelay(() -> Events.fire(BlockBuildEvent.class, fteam, tile)); } /**Returns whether a tile can be placed at this location by this team.*/ diff --git a/core/src/io/anuke/mindustry/world/blocks/BreakBlock.java b/core/src/io/anuke/mindustry/world/blocks/BreakBlock.java index efecd7ebcc..b5930297be 100644 --- a/core/src/io/anuke/mindustry/world/blocks/BreakBlock.java +++ b/core/src/io/anuke/mindustry/world/blocks/BreakBlock.java @@ -140,9 +140,12 @@ public class BreakBlock extends Block { @Remote(called = Loc.server, in = In.blocks) public static void onBreakFinish(Tile tile){ - BreakEntity entity = tile.entity(); - Effects.effect(Fx.breakBlock, tile.drawx(), tile.drawy(), entity.previous.size); + if(tile.entity instanceof BreakEntity){ + BreakEntity entity = tile.entity(); + Effects.effect(Fx.breakBlock, tile.drawx(), tile.drawy(), entity.previous.size); + } + world.removeBlock(tile); } diff --git a/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java b/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java index f16369d88d..220400590b 100644 --- a/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java +++ b/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java @@ -1,8 +1,10 @@ package io.anuke.mindustry.desktop; import io.anuke.mindustry.net.Net; +import io.anuke.ucore.core.Settings; import io.anuke.ucore.util.Strings; +import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; import java.text.SimpleDateFormat; @@ -14,8 +16,6 @@ public class CrashHandler { //TODO send full error report to server via HTTP e.printStackTrace(); - - //attempt to close connections, if applicable try{ Net.dispose(); @@ -26,8 +26,20 @@ public class CrashHandler { //don't create crash logs for me (anuke), as it's expected if(System.getProperty("user.name").equals("anuke")) return; + String header = ""; + + try{ + header += "--GAME INFO-- \n"; + header += "Multithreading: " + Settings.getBool("multithread")+ "\n"; + header += "Net Active: " + Net.active()+ "\n"; + header += "Net Server: " + Net.server()+ "\n"; + header += "OS: " + System.getProperty("os.name")+ "\n----\n"; + }catch (Throwable e4){ + e4.printStackTrace(); + } + //parse exception - String result = Strings.parseException(e, true); + String result = header + Strings.parseFullException(e); boolean failed = false; String filename = ""; @@ -42,9 +54,9 @@ public class CrashHandler { } try{ - //JOptionPane.showMessageDialog(null, "An error has occured: \n" + result + "\n\n" + - // (!failed ? "A crash report has been written to " + new File(filename).getAbsolutePath() + ".\nPlease send this file to the developer!" - // : "Failed to generate crash report.\nPlease send an image of this crash log to the developer!")); + javax.swing.JOptionPane.showMessageDialog(null, "An error has occured: \n" + result + "\n\n" + + (!failed ? "A crash report has been written to " + new File(filename).getAbsolutePath() + ".\nPlease send this file to the developer!" + : "Failed to generate crash report.\nPlease send an image of this crash log to the developer!")); }catch (Throwable i){ i.printStackTrace(); //what now? diff --git a/kryonet/src/io/anuke/kryonet/CustomListeners.java b/kryonet/src/io/anuke/kryonet/CustomListeners.java new file mode 100644 index 0000000000..4511926459 --- /dev/null +++ b/kryonet/src/io/anuke/kryonet/CustomListeners.java @@ -0,0 +1,89 @@ +package io.anuke.kryonet; + +import com.esotericsoftware.kryonet.Connection; +import com.esotericsoftware.kryonet.Listener; +import com.esotericsoftware.kryonet.Listener.QueuedListener; + +import java.util.LinkedList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class CustomListeners { + + static public class LagListener extends QueuedListener { + protected final ScheduledExecutorService threadPool; + private final int lagMillisMin, lagMillisMax; + final LinkedList runnables = new LinkedList(); + + public LagListener (int lagMillisMin, int lagMillisMax, Listener listener) { + super(listener); + this.lagMillisMin = lagMillisMin; + this.lagMillisMax = lagMillisMax; + threadPool = Executors.newScheduledThreadPool(1, r -> { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + return t; + }); + } + + protected int calculateLag() { + return lagMillisMin + (int)(Math.random() * (lagMillisMax - lagMillisMin)); + } + + @Override + public void queue (Runnable runnable) { + + synchronized (runnables) { + runnables.addFirst(runnable); + } + threadPool.schedule(() -> { + Runnable runnable1; + synchronized (runnables) { + runnable1 = runnables.removeLast(); + } + runnable1.run(); + }, calculateLag(), TimeUnit.MILLISECONDS); + } + } + + /** + * Delays, reorders and does not make guarantees to the delivery of incoming objects + * to the wrapped listener (in order to simulate lag, jitter, package loss and + * package duplication). + * Notification events are likely processed on a separate thread after a delay. + * Note that only the delivery of incoming objects is modified. To modify the delivery + * of outgoing objects, use a UnreliableListener at the other end of the connection. + */ + static public class UnreliableListener extends LagListener { + private final float lossPercentage; + private final float duplicationPercentage; + private final CustomListeners.LagListener tcpListener; + + public UnreliableListener (int lagMillisMin, int lagMillisMax, float lossPercentage, + float duplicationPercentage, Listener listener) { + super(lagMillisMin, lagMillisMax, listener); + this.tcpListener = new CustomListeners.LagListener(lagMillisMin, lagMillisMax, listener); + this.lossPercentage = lossPercentage; + this.duplicationPercentage = duplicationPercentage; + } + + @Override + public void received(Connection connection, Object object) { + if(KryoCore.lastUDP) { + super.received(connection, object); + }else{ + tcpListener.received(connection, object); + } + } + + @Override + public void queue (Runnable runnable) { + do { + if (Math.random() >= lossPercentage) { + threadPool.schedule(runnable, calculateLag(), TimeUnit.MILLISECONDS); + } + } while (Math.random() < duplicationPercentage); + } + } +} diff --git a/kryonet/src/io/anuke/kryonet/KryoClient.java b/kryonet/src/io/anuke/kryonet/KryoClient.java index 578ac741ad..dca0cbdab6 100644 --- a/kryonet/src/io/anuke/kryonet/KryoClient.java +++ b/kryonet/src/io/anuke/kryonet/KryoClient.java @@ -5,8 +5,8 @@ import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.ObjectSet; import com.esotericsoftware.kryonet.*; -import com.esotericsoftware.kryonet.Listener.LagListener; import com.esotericsoftware.minlog.Log; +import io.anuke.kryonet.CustomListeners.UnreliableListener; import io.anuke.mindustry.net.Host; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.Net.ClientProvider; @@ -25,9 +25,7 @@ import java.nio.ByteBuffer; import java.nio.channels.ClosedSelectorException; import java.util.List; -import static io.anuke.mindustry.Vars.netClient; -import static io.anuke.mindustry.Vars.port; -import static io.anuke.mindustry.Vars.threads; +import static io.anuke.mindustry.Vars.*; public class KryoClient implements ClientProvider{ Client client; @@ -35,6 +33,8 @@ public class KryoClient implements ClientProvider{ ClientDiscoveryHandler handler; public KryoClient(){ + KryoCore.init(); + handler = new ClientDiscoveryHandler() { @Override public DatagramPacket onRequestNewDatagramPacket() { @@ -80,7 +80,7 @@ public class KryoClient implements ClientProvider{ public void received (Connection connection, Object object) { if(object instanceof FrameworkMessage) return; - Gdx.app.postRunnable(() -> { + threads.runDelay(() -> { try{ Net.handleClientReceived(object); }catch (Exception e){ @@ -97,8 +97,8 @@ public class KryoClient implements ClientProvider{ } }; - if(KryoRegistrator.fakeLag){ - client.addListener(new LagListener(KryoRegistrator.fakeLagMin, KryoRegistrator.fakeLagMax, listener)); + if(KryoCore.fakeLag){ + client.addListener(new UnreliableListener(KryoCore.fakeLagMin, KryoCore.fakeLagMax, KryoCore.fakeLagDrop, KryoCore.fakeLagDuplicate, listener)); }else{ client.addListener(listener); } diff --git a/kryonet/src/io/anuke/kryonet/KryoCore.java b/kryonet/src/io/anuke/kryonet/KryoCore.java new file mode 100644 index 0000000000..8b4d9bc8e4 --- /dev/null +++ b/kryonet/src/io/anuke/kryonet/KryoCore.java @@ -0,0 +1,89 @@ +package io.anuke.kryonet; + +import com.esotericsoftware.minlog.Log; +import com.esotericsoftware.minlog.Log.Logger; +import io.anuke.ucore.util.ColorCodes; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static io.anuke.mindustry.Vars.headless; + +/**Utilities and configs for kryo module.*/ +public class KryoCore { + public static boolean fakeLag = false; + public static final int fakeLagMax = 500; + public static final int fakeLagMin = 0; + public static final float fakeLagDrop = 0.1f; + public static final float fakeLagDuplicate = 0.1f; + + public static boolean lastUDP; + + private static ScheduledExecutorService threadPool; + + public static void init(){ + Log.set(fakeLag ? Log.LEVEL_DEBUG : Log.LEVEL_WARN); + + Log.setLogger(new Logger(){ + public void log (int level, String category, String message, Throwable ex) { + if(fakeLag){ + if(message.contains("UDP")){ + lastUDP = true; + }else if(message.contains("TCP")){ + lastUDP = false; + } + return; + } + + StringBuilder builder = new StringBuilder(256); + + if(headless) + builder.append(ColorCodes.BLUE); + + builder.append("Net Error: "); + + builder.append(message); + + if (ex != null) { + StringWriter writer = new StringWriter(256); + ex.printStackTrace(new PrintWriter(writer)); + builder.append('\n'); + builder.append(writer.toString().trim()); + } + + if(headless) + builder.append(ColorCodes.RESET); + + io.anuke.ucore.util.Log.info("&b" + builder.toString()); + } + }); + } + + private static int calculateLag() { + return fakeLagMin + (int)(Math.random() * (fakeLagMax - fakeLagMin)); + } + + /**Executes something in a potentially unreliable way. Used to simulate lag and packet errors with UDP.*/ + public static void recieveUnreliable(Runnable run){ + if(fakeLag && threadPool == null){ + threadPool = Executors.newScheduledThreadPool(1, r -> { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + return t; + }); + } + + if(fakeLag){ + do { + if (Math.random() >= fakeLagDrop) { + threadPool.schedule(run, calculateLag(), TimeUnit.MILLISECONDS); + } + } while (Math.random() < fakeLagDuplicate); + }else{ + run.run(); + } + } +} diff --git a/kryonet/src/io/anuke/kryonet/KryoRegistrator.java b/kryonet/src/io/anuke/kryonet/KryoRegistrator.java deleted file mode 100644 index 5d9590dd27..0000000000 --- a/kryonet/src/io/anuke/kryonet/KryoRegistrator.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.anuke.kryonet; - -import com.esotericsoftware.minlog.Log; -import com.esotericsoftware.minlog.Log.Logger; -import io.anuke.ucore.util.ColorCodes; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import static io.anuke.mindustry.Vars.headless; - -public class KryoRegistrator { - public static boolean fakeLag = false; - public static final int fakeLagMax = 1000; - public static final int fakeLagMin = 0; - - static{ - Log.set(Log.LEVEL_WARN); - - Log.setLogger(new Logger(){ - public void log (int level, String category, String message, Throwable ex) { - StringBuilder builder = new StringBuilder(256); - - if(headless) - builder.append(ColorCodes.BLUE); - - builder.append("Net Error: "); - - builder.append(message); - - if (ex != null) { - StringWriter writer = new StringWriter(256); - ex.printStackTrace(new PrintWriter(writer)); - builder.append('\n'); - builder.append(writer.toString().trim()); - } - - if(headless) - builder.append(ColorCodes.RESET); - - io.anuke.ucore.util.Log.info("&b" + builder.toString()); - } - }); - } -} diff --git a/kryonet/src/io/anuke/kryonet/KryoServer.java b/kryonet/src/io/anuke/kryonet/KryoServer.java index 987dbe4a04..af8f06dbe5 100644 --- a/kryonet/src/io/anuke/kryonet/KryoServer.java +++ b/kryonet/src/io/anuke/kryonet/KryoServer.java @@ -6,9 +6,9 @@ import com.badlogic.gdx.utils.Base64Coder; import com.esotericsoftware.kryonet.Connection; import com.esotericsoftware.kryonet.FrameworkMessage; import com.esotericsoftware.kryonet.Listener; -import com.esotericsoftware.kryonet.Listener.LagListener; import com.esotericsoftware.kryonet.Server; import com.esotericsoftware.kryonet.util.InputStreamSender; +import io.anuke.kryonet.CustomListeners.UnreliableListener; import io.anuke.mindustry.Vars; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.Net.SendMode; @@ -17,9 +17,9 @@ import io.anuke.mindustry.net.NetConnection; import io.anuke.mindustry.net.NetworkIO; import io.anuke.mindustry.net.Packets.Connect; import io.anuke.mindustry.net.Packets.Disconnect; -import io.anuke.mindustry.net.Streamable; import io.anuke.mindustry.net.Packets.StreamBegin; import io.anuke.mindustry.net.Packets.StreamChunk; +import io.anuke.mindustry.net.Streamable; import io.anuke.ucore.UCore; import io.anuke.ucore.core.Timers; import io.anuke.ucore.util.Log; @@ -52,6 +52,8 @@ public class KryoServer implements ServerProvider { int lastconnection = 0; public KryoServer(){ + KryoCore.init(); + server = new Server(4096*2, 4096, connection -> new ByteSerializer()); server.setDiscoveryHandler((datagramChannel, fromAddress) -> { ByteBuffer buffer = NetworkIO.writeServerData(); @@ -110,8 +112,8 @@ public class KryoServer implements ServerProvider { } }; - if(KryoRegistrator.fakeLag){ - server.addListener(new LagListener(KryoRegistrator.fakeLagMin, KryoRegistrator.fakeLagMax, listener)); + if(KryoCore.fakeLag){ + server.addListener(new UnreliableListener(KryoCore.fakeLagMin, KryoCore.fakeLagMax, KryoCore.fakeLagDrop, KryoCore.fakeLagDuplicate, listener)); }else{ server.addListener(listener); } @@ -323,6 +325,11 @@ public class KryoServer implements ServerProvider { this.connection = connection; } + @Override + public boolean isConnected(){ + return connection == null ? !socket.isClosed() : connection.isConnected(); + } + @Override public void send(Object object, SendMode mode){ if(socket != null){