diff --git a/build.gradle b/build.gradle index cfc4e155fb..c1b7b7f497 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,8 @@ project(":html") { compile "com.badlogicgames.gdx:gdx-controllers:$gdxVersion:sources" compile "com.badlogicgames.gdx:gdx-controllers-gwt:$gdxVersion" compile "com.badlogicgames.gdx:gdx-controllers-gwt:$gdxVersion:sources" + compile "com.sksamuel.gwt:gwt-websockets:1.0.4" + compile "com.sksamuel.gwt:gwt-websockets:1.0.4:sources" } } @@ -116,6 +118,7 @@ project(":kryonet") { dependencies { compile project(":core") compile 'com.github.crykn:kryonet:2.22.1' + compile "org.java-websocket:Java-WebSocket:1.3.7" } } diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 671b3db77a..4690a1c9a6 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -16,6 +16,7 @@ text.about.button=About text.name=Name: text.public=Public text.players={0} players online +text.server.player.host={0} (host) text.players.single={0} player online text.server.mismatch=Packet error: possible client/server version mismatch.\nMake sure you and the host have the\nlatest version of Mindustry! text.server.closing=[accent]Closing server... @@ -45,6 +46,7 @@ text.connecting=[accent]Connecting... text.connecting.data=[accent]Loading world data... text.connectfail=[crimson]Failed to connect to server: [orange]{0} text.server.port=Port: +text.server.addressinuse=Address already in use! text.server.invalidport=Invalid port number! text.server.error=[crimson]Error hosting server: [orange]{0} text.tutorial.back=< Prev diff --git a/core/src/Mindustry.gwt.xml b/core/src/Mindustry.gwt.xml index b78f7e2301..68c562e7dd 100644 --- a/core/src/Mindustry.gwt.xml +++ b/core/src/Mindustry.gwt.xml @@ -7,4 +7,7 @@ + + + \ No newline at end of file diff --git a/core/src/io/anuke/mindustry/Mindustry.java b/core/src/io/anuke/mindustry/Mindustry.java index c6dbd7f055..e844d43ec3 100644 --- a/core/src/io/anuke/mindustry/Mindustry.java +++ b/core/src/io/anuke/mindustry/Mindustry.java @@ -38,6 +38,7 @@ public class Mindustry extends ModuleCore { @Override public void dispose() { platforms.onGameExit(); + Net.dispose(); super.dispose(); } diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index 89b8f38e3a..ce43ca4485 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -75,6 +75,7 @@ public class Vars{ //server port public static final int port = 6567; + public static final int webPort = 6568; public static Control control; public static Renderer renderer; diff --git a/core/src/io/anuke/mindustry/core/Control.java b/core/src/io/anuke/mindustry/core/Control.java index a5148165ee..06f121405a 100644 --- a/core/src/io/anuke/mindustry/core/Control.java +++ b/core/src/io/anuke/mindustry/core/Control.java @@ -546,7 +546,6 @@ public class Control extends Module{ @Override public void update(){ - //UCore.log(input.placeMode, input.breakMode, input.lastPlaceMode); if(Gdx.input != proxy){ Gdx.input = proxy; diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index 0a3f81c5e4..d3a24a5179 100644 --- a/core/src/io/anuke/mindustry/core/NetClient.java +++ b/core/src/io/anuke/mindustry/core/NetClient.java @@ -272,12 +272,21 @@ public class NetClient extends Module { } }); - Net.handle(Player.class, player -> { + Net.handle(PlayerSpawnPacket.class, packet -> { Gdx.app.postRunnable(() -> { //duplicates. - if(Vars.control.enemyGroup.getByID(player.id) != null || - recieved.contains(player.id)) return; - recieved.add(player.id); + if(Vars.control.enemyGroup.getByID(packet.id) != null || + recieved.contains(packet.id)) return; + recieved.add(packet.id); + + Player player = new Player(); + player.x = packet.x; + player.y = packet.y; + player.isAndroid = packet.android; + player.name = packet.name; + player.id = packet.id; + player.weaponLeft = (Weapon) Upgrade.getByID(packet.weaponleft); + player.weaponRight = (Weapon) Upgrade.getByID(packet.weaponright); player.interpolator.last.set(player.x, player.y); player.interpolator.target.set(player.x, player.y); @@ -291,7 +300,7 @@ public class NetClient extends Module { kicked = true; Net.disconnect(); GameState.set(State.menu); - Gdx.app.postRunnable(() -> Vars.ui.showError("$text.server.kicked." + KickReason.values()[packet.reason].name())); + Gdx.app.postRunnable(() -> Vars.ui.showError("$text.server.kicked." + packet.reason.name())); }); Net.handle(WeaponSwitchPacket.class, packet -> { diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index ad1d7e02af..218fb0fa5e 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -9,6 +9,7 @@ import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.entities.SyncEntity; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.entities.enemies.Enemy; +import io.anuke.mindustry.net.NetConnection; import io.anuke.mindustry.net.NetworkIO; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.Net.SendMode; @@ -190,7 +191,16 @@ public class NetServer extends Module{ int dest = Net.getLastConnection(); Gdx.app.postRunnable(() -> { if(Vars.control.playerGroup.getByID(id) != null){ - Net.sendTo(dest, Vars.control.playerGroup.getByID(id), SendMode.tcp); + Player player = Vars.control.playerGroup.getByID(id); + PlayerSpawnPacket p = new PlayerSpawnPacket(); + p.x = player.x; + p.y = player.y; + p.id = player.id; + p.name = player.name; + p.weaponleft = player.weaponLeft.id; + p.weaponright = player.weaponRight.id; + p.android = player.isAndroid; + Net.sendTo(dest, p, SendMode.tcp); Gdx.app.error("Mindustry", "Replying to entity request ("+Net.getLastConnection()+"): player, " + id); }else if (Vars.control.enemyGroup.getByID(id) != null){ Enemy enemy = Vars.control.enemyGroup.getByID(id); @@ -361,10 +371,10 @@ public class NetServer extends Module{ if(Timers.get("serverBlockSync", blockSyncTime)){ - IntArray connections = Net.getConnections(); + Array connections = new Array<>(); for(int i = 0; i < connections.size; i ++){ - int id = connections.get(i); + int id = connections.get(i).id; Player player = this.connections.get(id); if(player == null) continue; int x = Mathf.scl2(player.x, Vars.tilesize); diff --git a/core/src/io/anuke/mindustry/entities/BulletType.java b/core/src/io/anuke/mindustry/entities/BulletType.java index fae051e255..7236e90fd2 100644 --- a/core/src/io/anuke/mindustry/entities/BulletType.java +++ b/core/src/io/anuke/mindustry/entities/BulletType.java @@ -352,7 +352,7 @@ public abstract class BulletType extends BaseBulletType{ public void draw(Bullet b){ Draw.thick(2f); Draw.color(lightOrange, Color.WHITE, 0.4f); - Draw.polygon(b.y, b.x, 3, 1.6f, b.angle()); + Draw.polygon(b.x, b.y, 3, 1.6f, b.angle()); Draw.thick(1f); Draw.color(Color.WHITE, lightOrange, b.ifract()/2f); Draw.alpha(b.ifract()); diff --git a/core/src/io/anuke/mindustry/entities/effect/EMP.java b/core/src/io/anuke/mindustry/entities/effect/EMP.java index 1f597faa3f..39891a9cb3 100644 --- a/core/src/io/anuke/mindustry/entities/effect/EMP.java +++ b/core/src/io/anuke/mindustry/entities/effect/EMP.java @@ -84,7 +84,7 @@ public class EMP extends TimedEntity{ } Draw.thick(fract()*2f); - Draw.polygon(y, x, 34, radius * Vars.tilesize); + Draw.polygon(x, y, 34, radius * Vars.tilesize); Draw.reset(); } diff --git a/core/src/io/anuke/mindustry/graphics/BlockRenderer.java b/core/src/io/anuke/mindustry/graphics/BlockRenderer.java index 332ae2030b..5540e72dde 100644 --- a/core/src/io/anuke/mindustry/graphics/BlockRenderer.java +++ b/core/src/io/anuke/mindustry/graphics/BlockRenderer.java @@ -1,21 +1,17 @@ package io.anuke.mindustry.graphics; -import static io.anuke.mindustry.Vars.*; -import static io.anuke.ucore.core.Core.camera; - -import java.util.Arrays; - import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.utils.Array; - import io.anuke.mindustry.Vars; import io.anuke.mindustry.core.GameState; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.game.SpawnPoint; -import io.anuke.mindustry.world.*; +import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Layer; +import io.anuke.mindustry.world.Tile; import io.anuke.mindustry.world.blocks.Blocks; import io.anuke.mindustry.world.blocks.types.StaticBlock; import io.anuke.ucore.core.Core; @@ -24,6 +20,11 @@ import io.anuke.ucore.core.Graphics; import io.anuke.ucore.graphics.CacheBatch; import io.anuke.ucore.util.Mathf; +import java.util.Arrays; + +import static io.anuke.mindustry.Vars.*; +import static io.anuke.ucore.core.Core.camera; + public class BlockRenderer{ private final static int chunksize = 32; private final static int initialRequests = 32*32; @@ -269,6 +270,6 @@ public class BlockRenderer{ cache = null; if(cbatch != null) cbatch.dispose(); - cbatch = new CacheBatch(Vars.world.width() * Vars.world.height() * 3); + cbatch = new CacheBatch(Vars.world.width() * Vars.world.height() * 4); } } diff --git a/core/src/io/anuke/mindustry/graphics/Fx.java b/core/src/io/anuke/mindustry/graphics/Fx.java index a0a99490d7..a67d5ccf9b 100644 --- a/core/src/io/anuke/mindustry/graphics/Fx.java +++ b/core/src/io/anuke/mindustry/graphics/Fx.java @@ -121,7 +121,7 @@ public class Fx{ nuclearShockwave = new Effect(10f, 200f, e -> { Draw.color(Color.WHITE, Color.LIGHT_GRAY, e.ifract()); Draw.thick(e.fract()*3f + 0.2f); - Draw.polygon(e.y, e.x, 40, e.ifract()*140f); + Draw.polygon(e.x, e.y, 40, e.ifract()*140f); Draw.reset(); }), @@ -452,7 +452,7 @@ public class Fx{ clusterbomb = new Effect(10f, e -> { Draw.color(Color.WHITE, lightOrange, e.ifract()); Draw.thick(e.fract()*1.5f); - Draw.polygon(e.y, e.x, 4, e.fract()*8f); + Draw.polygon(e.x, e.y, 4, e.fract()*8f); Draw.circle(e.x, e.y, e.ifract()*14f); Draw.reset(); }), diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index 8e2f09d1d5..2d42e5f80e 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -92,7 +92,7 @@ public class DesktopInput extends InputHandler{ for(int i = 1; i <= 6 && i <= control.getWeapons().size; i ++){ if(Inputs.keyTap("weapon_" + i)){ player.weaponLeft = player.weaponRight = control.getWeapons().get(i - 1); - Vars.netClient.handleWeaponSwitch(); + if(Net.active()) Vars.netClient.handleWeaponSwitch(); Vars.ui.hudfrag.updateWeapons(); } } diff --git a/core/src/io/anuke/mindustry/input/PlaceMode.java b/core/src/io/anuke/mindustry/input/PlaceMode.java index d75256573f..9773804bd1 100644 --- a/core/src/io/anuke/mindustry/input/PlaceMode.java +++ b/core/src/io/anuke/mindustry/input/PlaceMode.java @@ -96,7 +96,7 @@ public enum PlaceMode{ if(android && control.getInput().breaktime > 0){ Draw.color(Colors.get("breakStart"), Colors.get("break"), fract); - Draw.polygon(tile.worldx() + offset.y, tile.worldy() + offset.x, 25, 4 + (1f - fract) * 26); + Draw.polygon(tile.worldx() + offset.x, tile.worldy() + offset.y, 25, 4 + (1f - fract) * 26); } Draw.reset(); } diff --git a/core/src/io/anuke/mindustry/mapeditor/MapView.java b/core/src/io/anuke/mindustry/mapeditor/MapView.java index 0cc7ddabc1..29f04ce1dd 100644 --- a/core/src/io/anuke/mindustry/mapeditor/MapView.java +++ b/core/src/io/anuke/mindustry/mapeditor/MapView.java @@ -238,9 +238,9 @@ public class MapView extends Element implements GestureListener{ Draw.thick(Unit.dp.scl(3f * zoom)); Draw.line(sx, sy, v2.x, v2.y); - Draw.polygon(sy, sx, 40, editor.getBrushSize() * zoom * 3); + Draw.polygon(sx, sy, 40, editor.getBrushSize() * zoom * 3); - Draw.polygon(v2.y, v2.x, 40, editor.getBrushSize() * zoom * 3); + Draw.polygon(v2.x, v2.y, 40, editor.getBrushSize() * zoom * 3); } batch.flush(); diff --git a/core/src/io/anuke/mindustry/net/Net.java b/core/src/io/anuke/mindustry/net/Net.java index 196c6d6868..ee997018ef 100644 --- a/core/src/io/anuke/mindustry/net/Net.java +++ b/core/src/io/anuke/mindustry/net/Net.java @@ -2,7 +2,6 @@ package io.anuke.mindustry.net; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.utils.Array; -import com.badlogic.gdx.utils.IntArray; import com.badlogic.gdx.utils.IntMap; import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.async.AsyncExecutor; @@ -85,7 +84,7 @@ public class Net{ } /**Returns a list of all connections IDs.*/ - public static IntArray getConnections(){ + public static Array getConnections(){ return serverProvider.getConnections(); } @@ -206,8 +205,8 @@ public class Net{ } public static void dispose(){ - clientProvider.dispose(); - serverProvider.dispose(); + if(clientProvider != null) clientProvider.dispose(); + if(serverProvider != null) serverProvider.dispose(); executor.dispose(); } @@ -218,51 +217,51 @@ public class Net{ } /**Client implementation.*/ - public static interface ClientProvider { + public interface ClientProvider { /**Connect to a server.*/ - public void connect(String ip, int port) throws IOException; + void connect(String ip, int port) throws IOException; /**Send an object to the server.*/ - public void send(Object object, SendMode mode); + void send(Object object, SendMode mode); /**Update the ping. Should be done every second or so.*/ - public void updatePing(); + void updatePing(); /**Get ping in milliseconds. Will only be valid after a call to updatePing.*/ - public int getPing(); + int getPing(); /**Disconnect from the server.*/ - public void disconnect(); + void disconnect(); /**Discover servers. This should block for a certain amount of time, and will most likely be run in a different thread.*/ - public Array discover(); + Array discover(); /**Ping a host. If an error occured, failed() should be called with the exception. */ - public void pingHost(String address, int port, Consumer valid, Consumer failed); + void pingHost(String address, int port, Consumer valid, Consumer failed); /**Register classes to be sent.*/ - public void register(Class... types); + void register(Class... types); /**Close all connections.*/ - public void dispose(); + void dispose(); } /**Server implementation.*/ - public static interface ServerProvider { + public interface ServerProvider { /**Host a server at specified port.*/ - public void host(int port) throws IOException; + void host(int port) throws IOException; /**Sends a large stream of data to a specific client.*/ - public void sendStream(int id, Streamable stream); + void sendStream(int id, Streamable stream); /**Send an object to everyone connected.*/ - public void send(Object object, SendMode mode); + void send(Object object, SendMode mode); /**Send an object to a specific client ID.*/ - public void sendTo(int id, Object object, SendMode mode); + void sendTo(int id, Object object, SendMode mode); /**Send an object to everyone except a client ID.*/ - public void sendExcept(int id, Object object, SendMode mode); + void sendExcept(int id, Object object, SendMode mode); /**Close the server connection.*/ - public void close(); + void close(); /**Return all connected users.*/ - public IntArray getConnections(); + Array getConnections(); /**Kick a certain connection.*/ - public void kick(int connection); + void kick(int connection); /**Returns the ping for a certain connection.*/ - public int getPingFor(int connection); + int getPingFor(NetConnection connection); /**Register classes to be sent.*/ - public void register(Class... types); + void register(Class... types); /**Close all connections.*/ - public void dispose(); + void dispose(); } public enum SendMode{ diff --git a/core/src/io/anuke/mindustry/net/NetConnection.java b/core/src/io/anuke/mindustry/net/NetConnection.java new file mode 100644 index 0000000000..ba6bfbf80c --- /dev/null +++ b/core/src/io/anuke/mindustry/net/NetConnection.java @@ -0,0 +1,16 @@ +package io.anuke.mindustry.net; + +import io.anuke.mindustry.net.Net.SendMode; + +public abstract class NetConnection { + public final int id; + public final String address; + + public NetConnection(int id, String address){ + this.id = id; + this.address = address; + } + + public abstract void send(Object object, SendMode mode); + public abstract void close(); +} diff --git a/core/src/io/anuke/mindustry/net/Packet.java b/core/src/io/anuke/mindustry/net/Packet.java new file mode 100644 index 0000000000..a2c0c6e1d3 --- /dev/null +++ b/core/src/io/anuke/mindustry/net/Packet.java @@ -0,0 +1,8 @@ +package io.anuke.mindustry.net; + +import java.nio.ByteBuffer; + +public interface Packet { + void read(ByteBuffer buffer); + void write(ByteBuffer buffer); +} diff --git a/core/src/io/anuke/mindustry/net/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index 0cdabc7280..b0db9dba22 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -1,9 +1,15 @@ package io.anuke.mindustry.net; +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.entities.SyncEntity; +import io.anuke.mindustry.resource.Item; + +import java.nio.ByteBuffer; + /**Class for storing all packets.*/ public class Packets { - public static class Connect { + public static class Connect{ public int id; public String addressTCP; } @@ -17,128 +23,477 @@ public class Packets { } - public static class SyncPacket{ + public static class SyncPacket implements Packet{ public byte[] data; + + @Override + public void write(ByteBuffer buffer) { + buffer.putShort((short)data.length); + buffer.put(data); + } + + @Override + public void read(ByteBuffer buffer) { + data = new byte[buffer.getShort()]; + buffer.get(data); + } } public static class BlockSyncPacket extends Streamable{ } - public static class ConnectPacket{ + public static class ConnectPacket implements Packet{ public String name; public boolean android; + + @Override + public void write(ByteBuffer buffer) { + buffer.put((byte)name.length()); + buffer.put(name.getBytes()); + buffer.put(android ? (byte)1 : 0); + } + + @Override + public void read(ByteBuffer buffer) { + byte length = buffer.get(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + name = new String(bytes); + android = buffer.get() == 1; + } } - public static class ConnectConfirmPacket{ + public static class ConnectConfirmPacket implements Packet{ + @Override + public void write(ByteBuffer buffer) { } + @Override + public void read(ByteBuffer buffer) { } } - public static class DisconnectPacket{ + public static class DisconnectPacket implements Packet{ public int playerid; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(playerid); + } + + @Override + public void read(ByteBuffer buffer) { + playerid = buffer.getInt(); + } } - public static class StateSyncPacket{ + public static class StateSyncPacket implements Packet{ public int[] items; public float countdown, time; public int enemies, wave; public long timestamp; + + @Override + public void write(ByteBuffer buffer) { + for(int i = 0; i < items.length; i ++){ + buffer.putInt(items[i]); + } + + buffer.putFloat(countdown); + buffer.putFloat(time); + buffer.putShort((short)enemies); + buffer.putShort((short)wave); + buffer.putLong(timestamp); + } + + @Override + public void read(ByteBuffer buffer) { + items = new int[Item.getAllItems().size]; + + for(int i = 0; i < items.length; i ++){ + items[i] = buffer.getInt(); + } + + countdown = buffer.getFloat(); + time = buffer.getFloat(); + enemies = buffer.getShort(); + wave = buffer.getShort(); + timestamp = buffer.getLong(); + } } - public static class PositionPacket{ + public static class PositionPacket implements Packet{ public byte[] data; + + @Override + public void write(ByteBuffer buffer) { + buffer.put(data); + } + + @Override + public void read(ByteBuffer buffer) { + data = new byte[SyncEntity.getWriteSize(Player.class)]; + buffer.get(data); + } } + //not a real packet. public static class EffectPacket{ public int id; public float x, y, rotation; public int color; } - public static class ShootPacket{ + public static class ShootPacket implements Packet{ public byte weaponid; public float x, y, rotation; public int playerid; + + @Override + public void write(ByteBuffer buffer) { + buffer.put(weaponid); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putFloat(rotation); + buffer.putInt(playerid); + } + + @Override + public void read(ByteBuffer buffer) { + weaponid = buffer.get(); + x = buffer.getFloat(); + y = buffer.getFloat(); + rotation = buffer.getFloat(); + playerid = buffer.getInt(); + } } - public static class BulletPacket{ + public static class BulletPacket implements Packet{ public int type, owner; public float x, y, angle; public short damage; + + @Override + public void write(ByteBuffer buffer) { + buffer.putShort((short)type); + buffer.putInt(owner); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putFloat(angle); + buffer.putShort(damage); + } + + @Override + public void read(ByteBuffer buffer) { + type = buffer.getShort(); + owner = buffer.getInt(); + x = buffer.getFloat(); + y = buffer.getFloat(); + angle = buffer.getFloat(); + damage = buffer.getShort(); + } } - public static class PlacePacket{ + public static class PlacePacket implements Packet{ public int playerid; public byte rotation; public short x, y; public int block; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(playerid); + buffer.put(rotation); + buffer.putShort(x); + buffer.putShort(y); + buffer.putInt(block); + } + + @Override + public void read(ByteBuffer buffer) { + playerid = buffer.getInt(); + rotation = buffer.get(); + x = buffer.getShort(); + y = buffer.getShort(); + block = buffer.getInt(); + } } - public static class BreakPacket{ + public static class BreakPacket implements Packet{ public int playerid; public short x, y; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(playerid); + buffer.putShort(x); + buffer.putShort(y); + } + + @Override + public void read(ByteBuffer buffer) { + playerid = buffer.getInt(); + x = buffer.getShort(); + y = buffer.getShort(); + } } - public static class EnemySpawnPacket{ + public static class EnemySpawnPacket implements Packet{ public byte type, lane, tier; public float x, y; public short health; public int id; + + @Override + public void write(ByteBuffer buffer) { + buffer.put(type); + buffer.put(lane); + buffer.put(tier); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putShort(health); + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + type = buffer.get(); + lane = buffer.get(); + tier = buffer.get(); + x = buffer.getFloat(); + y = buffer.getFloat(); + health = buffer.getShort(); + id = buffer.getInt(); + } } - public static class EnemyDeathPacket{ + public static class PlayerSpawnPacket implements Packet{ + public byte weaponleft, weaponright; + public boolean android; + public String name; + public float x, y; public int id; + + @Override + public void write(ByteBuffer buffer) { + buffer.put((byte)name.length()); + buffer.put(name.getBytes()); + buffer.put(weaponleft); + buffer.put(weaponright); + buffer.put(android ? 1 : (byte)0); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + byte nlength = buffer.get(); + byte[] n = new byte[nlength]; + buffer.get(n); + name = new String(n); + weaponleft = buffer.get(); + weaponright = buffer.get(); + android = buffer.get() == 1; + x = buffer.getFloat(); + y = buffer.getFloat(); + id = buffer.getInt(); + } } - public static class BlockDestroyPacket{ + public static class EnemyDeathPacket implements Packet{ + public int id; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + id = buffer.getInt(); + } + } + + public static class BlockDestroyPacket implements Packet{ public int position; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(position); + } + + @Override + public void read(ByteBuffer buffer) { + position = buffer.getInt(); + } } - public static class BlockUpdatePacket{ + public static class BlockUpdatePacket implements Packet{ public int health, position; + + @Override + public void write(ByteBuffer buffer) { + buffer.putShort((short)health); + buffer.putInt(position); + } + + @Override + public void read(ByteBuffer buffer) { + health = buffer.getShort(); + position = buffer.getInt(); + } } - public static class ChatPacket{ + public static class ChatPacket implements Packet{ public String name; public String text; public int id; + + @Override + public void write(ByteBuffer buffer) { + if(name != null) { + buffer.putShort((short) name.length()); + buffer.put(name.getBytes()); + }else{ + buffer.putShort((short)-1); + } + buffer.putShort((short)text.length()); + buffer.put(text.getBytes()); + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + short nlength = buffer.getShort(); + if(nlength != -1) { + byte[] n = new byte[nlength]; + buffer.get(n); + name = new String(n); + } + short tlength = buffer.getShort(); + byte[] t = new byte[tlength]; + buffer.get(t); + + id = buffer.getInt(); + text = new String(t); + } } - public static class KickPacket{ - public byte reason; + public static class KickPacket implements Packet{ + public KickReason reason; + + @Override + public void write(ByteBuffer buffer) { + buffer.put((byte)reason.ordinal()); + } + + @Override + public void read(ByteBuffer buffer) { + reason = KickReason.values()[buffer.get()]; + } } public enum KickReason{ kick, invalidPassword } - public static class UpgradePacket{ + public static class UpgradePacket implements Packet{ public byte id; //weapon ID only, currently + + @Override + public void write(ByteBuffer buffer) { + buffer.put(id); + } + + @Override + public void read(ByteBuffer buffer) { + id = buffer.get(); + } } - public static class WeaponSwitchPacket{ + public static class WeaponSwitchPacket implements Packet{ public int playerid; public byte left, right; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(playerid); + buffer.put(left); + buffer.put(right); + } + + @Override + public void read(ByteBuffer buffer) { + playerid = buffer.getInt(); + left = buffer.get(); + right = buffer.get(); + } } - public static class BlockTapPacket{ + public static class BlockTapPacket implements Packet{ public int position; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(position); + } + + @Override + public void read(ByteBuffer buffer) { + position = buffer.getInt(); + } } - public static class BlockConfigPacket{ + public static class BlockConfigPacket implements Packet{ public int position; public byte data; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(position); + buffer.put(data); + } + + @Override + public void read(ByteBuffer buffer) { + position = buffer.getInt(); + data = buffer.get(); + } } - public static class EntityRequestPacket{ + public static class EntityRequestPacket implements Packet{ public int id; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + id = buffer.getInt(); + } } - public static class GameOverPacket{ + public static class GameOverPacket implements Packet{ + @Override + public void write(ByteBuffer buffer) { } + @Override + public void read(ByteBuffer buffer) { } } - public static class FriendlyFireChangePacket{ + public static class FriendlyFireChangePacket implements Packet{ public boolean enabled; + + @Override + public void write(ByteBuffer buffer) { + buffer.put(enabled ? 1 : (byte)0); + } + + @Override + public void read(ByteBuffer buffer) { + enabled = buffer.get() == 1; + } } } diff --git a/core/src/io/anuke/mindustry/net/Registrator.java b/core/src/io/anuke/mindustry/net/Registrator.java index a551a4c87e..6e740f862c 100644 --- a/core/src/io/anuke/mindustry/net/Registrator.java +++ b/core/src/io/anuke/mindustry/net/Registrator.java @@ -1,59 +1,62 @@ package io.anuke.mindustry.net; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.Array; -import io.anuke.mindustry.entities.Player; -import io.anuke.mindustry.entities.enemies.Enemy; +import com.badlogic.gdx.utils.ObjectIntMap; +import com.badlogic.gdx.utils.reflect.ClassReflection; import io.anuke.mindustry.net.Packets.*; import io.anuke.mindustry.net.Streamable.StreamBegin; import io.anuke.mindustry.net.Streamable.StreamChunk; -import io.anuke.ucore.entities.Entity; public class Registrator { + private static Class[] classes = { + StreamBegin.class, + StreamChunk.class, + WorldData.class, + SyncPacket.class, + PositionPacket.class, + ShootPacket.class, + PlacePacket.class, + BreakPacket.class, + StateSyncPacket.class, + BlockSyncPacket.class, + EnemySpawnPacket.class, + PlayerSpawnPacket.class, + BulletPacket.class, + EnemyDeathPacket.class, + BlockUpdatePacket.class, + BlockDestroyPacket.class, + ConnectPacket.class, + DisconnectPacket.class, + ChatPacket.class, + KickPacket.class, + UpgradePacket.class, + WeaponSwitchPacket.class, + BlockTapPacket.class, + BlockConfigPacket.class, + EntityRequestPacket.class, + ConnectConfirmPacket.class, + GameOverPacket.class, + FriendlyFireChangePacket.class, + }; + private static ObjectIntMap> ids = new ObjectIntMap<>(); + + static{ + if(classes.length > 127) throw new RuntimeException("Can't have more than 127 registered classes!"); + for(int i = 0; i < classes.length; i ++){ + if(!ClassReflection.isAssignableFrom(Packet.class, classes[i]) && + !ClassReflection.isAssignableFrom(Streamable.class, classes[i])) throw new RuntimeException("Not a packet: " + classes[i]); + ids.put(classes[i], i); + } + } + + public static Class getByID(byte id){ + return classes[id]; + } + + public static byte getID(Class type){ + return (byte)ids.get(type, -1); + } public static Class[] getClasses(){ - return new Class[]{ - StreamBegin.class, - StreamChunk.class, - WorldData.class, - SyncPacket.class, - PositionPacket.class, - ShootPacket.class, - PlacePacket.class, - BreakPacket.class, - StateSyncPacket.class, - BlockSyncPacket.class, - EnemySpawnPacket.class, - BulletPacket.class, - EnemyDeathPacket.class, - BlockUpdatePacket.class, - BlockDestroyPacket.class, - ConnectPacket.class, - DisconnectPacket.class, - ChatPacket.class, - KickPacket.class, - UpgradePacket.class, - WeaponSwitchPacket.class, - BlockTapPacket.class, - BlockConfigPacket.class, - EntityRequestPacket.class, - ConnectConfirmPacket.class, - GameOverPacket.class, - FriendlyFireChangePacket.class, - - Class.class, - byte[].class, - float[].class, - float[][].class, - int[].class, - int[][].class, - Entity[].class, - Array.class, - Vector2.class, - - Entity.class, - Player.class, - Enemy.class - }; + return classes; } } diff --git a/core/src/io/anuke/mindustry/net/Streamable.java b/core/src/io/anuke/mindustry/net/Streamable.java index a6f22be3ce..e115dd5bfb 100644 --- a/core/src/io/anuke/mindustry/net/Streamable.java +++ b/core/src/io/anuke/mindustry/net/Streamable.java @@ -6,23 +6,51 @@ import com.badlogic.gdx.utils.reflect.ReflectionException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; +import java.nio.ByteBuffer; -public class Streamable { +public class Streamable{ public transient ByteArrayInputStream stream; /**Marks the beginning of a stream.*/ - public static class StreamBegin{ + public static class StreamBegin implements Packet{ private static int lastid; public int id = lastid ++; public int total; public Class type; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(id); + buffer.putInt(total); + buffer.put(Registrator.getID(type)); + } + + @Override + public void read(ByteBuffer buffer) { + id = buffer.getInt(); + total = buffer.getInt(); + type = (Class)Registrator.getByID(buffer.get()); + } } - public static class StreamChunk{ + public static class StreamChunk implements Packet{ public int id; public byte[] data; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(id); + buffer.putShort((short)data.length); + buffer.put(data); + } + + @Override + public void read(ByteBuffer buffer) { + id = buffer.getInt(); + data = new byte[buffer.getShort()]; + buffer.get(data); + } } public static class StreamBuilder{ diff --git a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java index 1268a261d1..2598037933 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java @@ -11,7 +11,6 @@ import io.anuke.ucore.scene.style.Drawable; import io.anuke.ucore.scene.ui.Dialog; import io.anuke.ucore.scene.ui.ScrollPane; import io.anuke.ucore.scene.ui.TextButton; -import io.anuke.ucore.scene.ui.TextField.TextFieldFilter.DigitsOnlyFilter; import io.anuke.ucore.scene.ui.layout.Table; import io.anuke.ucore.util.Bundles; import io.anuke.ucore.util.Strings; @@ -33,19 +32,20 @@ public class JoinDialog extends FloatingDialog { addCloseButton(); join = new FloatingDialog("$text.joingame.title"); - join.content().add("$text.joingame.ip").left(); + join.content().add("$text.joingame.ip").padRight(5f).left(); Mindustry.platforms.addDialog(join.content().addField(Settings.getString("ip"),text ->{ Settings.putString("ip", text); Settings.save(); }).size(240f, 54f).get(), 100); join.content().row(); + /* join.content().add("$text.server.port").left(); Mindustry.platforms.addDialog(join.content() .addField(Settings.getString("port"), new DigitsOnlyFilter(), text ->{ Settings.putString("port", text); Settings.save(); - }).size(240f, 54f).get()); + }).size(240f, 54f).get());*/ join.buttons().defaults().size(140f, 60f).pad(4f); join.buttons().addButton("$text.cancel", join::hide); join.buttons().addButton("$text.ok", () -> { @@ -56,14 +56,14 @@ public class JoinDialog extends FloatingDialog { setupRemote(); refreshRemote(); }else{ - renaming.port = Strings.parseInt(Settings.getString("port")); + //renaming.port = Strings.parseInt(Settings.getString("port")); renaming.ip = Settings.getString("ip"); saveServers(); setupRemote(); refreshRemote(); } join.hide(); - }).disabled(b -> Settings.getString("ip").isEmpty() || Strings.parseInt(Settings.getString("port")) == Integer.MIN_VALUE || Net.active()); + }).disabled(b -> Settings.getString("ip").isEmpty() || Net.active()); join.shown(() -> { join.getTitleLabel().setText(renaming != null ? "$text.server.edit" : "$text.server.add"); @@ -83,8 +83,8 @@ public class JoinDialog extends FloatingDialog { //why are java lambdas this bad TextButton[] buttons = {null}; - TextButton button = buttons[0] = remote.addButton("[accent]"+server.ip + " / " + server.port, "clear", () -> { - if(!buttons[0].childrenPressed()) connect(server.ip, server.port); + TextButton button = buttons[0] = remote.addButton("[accent]"+server.ip, "clear", () -> { + if(!buttons[0].childrenPressed()) connect(server.ip, Vars.port); }).width(w).height(120f).pad(4f).get(); button.getLabel().setWrap(true); @@ -145,10 +145,12 @@ public class JoinDialog extends FloatingDialog { } void refreshLocal(){ - local.clear(); - local.background("button"); - local.label(() -> "[accent]" + Bundles.get("text.hosts.discovering") + new String(new char[(int)(Timers.time() / 10) % 4]).replace("\0", ".")).pad(10f); - Net.discoverServers(this::addLocalHosts); + if(!Vars.gwt) { + local.clear(); + local.background("button"); + local.label(() -> "[accent]" + Bundles.get("text.hosts.discovering") + new String(new char[(int) (Timers.time() / 10) % 4]).replace("\0", ".")).pad(10f); + Net.discoverServers(this::addLocalHosts); + } } void setup(){ @@ -201,7 +203,7 @@ public class JoinDialog extends FloatingDialog { button.add("[lightgray]" + (a.players != 1 ? Bundles.format("text.players", a.players) : Bundles.format("text.players.single", a.players))); button.row(); - button.add("[lightgray]" + a.address + " / " + Vars.port).pad(4).left(); + button.add("[lightgray]" + a.address).pad(4).left(); local.row(); local.background((Drawable) null); diff --git a/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java b/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java index e5fbeeb14a..47b28174aa 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java @@ -28,10 +28,9 @@ public class MenuFragment implements Fragment{ add(new MenuButton("$text.play", group, ui.levels::show)); row(); - if(!gwt){ - add(new MenuButton("$text.joingame", group, ui.join::show)); - row(); - } + add(new MenuButton("$text.joingame", group, ui.join::show)); + row(); + add(new MenuButton("$text.tutorial", group, ()-> control.playMap(world.maps().getMap("tutorial")))); row(); diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java index 4cf614c21f..07cd254e57 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java @@ -120,6 +120,7 @@ public class DesktopLauncher { Mindustry.args = Array.with(arg); + //Net.setClientProvider(new JavaWebsocketClient()); Net.setClientProvider(new KryoClient()); Net.setServerProvider(new KryoServer()); diff --git a/html/src/io/anuke/mindustry/GdxDefinition.gwt.xml b/html/src/io/anuke/mindustry/GdxDefinition.gwt.xml index f29f12e49e..f7ce42f271 100644 --- a/html/src/io/anuke/mindustry/GdxDefinition.gwt.xml +++ b/html/src/io/anuke/mindustry/GdxDefinition.gwt.xml @@ -6,6 +6,7 @@ + diff --git a/html/src/io/anuke/mindustry/client/HtmlLauncher.java b/html/src/io/anuke/mindustry/client/HtmlLauncher.java index 808dc291cf..9209ab2c88 100644 --- a/html/src/io/anuke/mindustry/client/HtmlLauncher.java +++ b/html/src/io/anuke/mindustry/client/HtmlLauncher.java @@ -17,6 +17,7 @@ import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.*; import io.anuke.mindustry.Mindustry; import io.anuke.mindustry.io.PlatformFunction; +import io.anuke.mindustry.net.Net; import java.util.Date; @@ -90,6 +91,8 @@ public class HtmlLauncher extends GwtApplication { setupResizeHook(); } }); + + Net.setClientProvider(new WebsocketClient()); Mindustry.platforms = new PlatformFunction(){ DateTimeFormat format = DateTimeFormat.getFormat("EEE, dd MMM yyyy HH:mm:ss"); diff --git a/html/src/io/anuke/mindustry/client/WebsocketClient.java b/html/src/io/anuke/mindustry/client/WebsocketClient.java new file mode 100644 index 0000000000..62bbb31d95 --- /dev/null +++ b/html/src/io/anuke/mindustry/client/WebsocketClient.java @@ -0,0 +1,146 @@ +package io.anuke.mindustry.client; + +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Base64Coder; +import com.badlogic.gdx.utils.reflect.ClassReflection; +import com.badlogic.gdx.utils.reflect.ReflectionException; +import com.sksamuel.gwt.websockets.Websocket; +import com.sksamuel.gwt.websockets.WebsocketListener; +import io.anuke.mindustry.Vars; +import io.anuke.mindustry.net.Host; +import io.anuke.mindustry.net.Net; +import io.anuke.mindustry.net.Net.ClientProvider; +import io.anuke.mindustry.net.Net.SendMode; +import io.anuke.mindustry.net.Packet; +import io.anuke.mindustry.net.Packets.Connect; +import io.anuke.mindustry.net.Packets.Disconnect; +import io.anuke.mindustry.net.Registrator; +import io.anuke.ucore.core.Timers; +import io.anuke.ucore.function.Consumer; +import io.anuke.ucore.util.Strings; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class WebsocketClient implements ClientProvider { + Websocket socket; + ByteBuffer buffer = ByteBuffer.allocate(1024); + + @Override + public void connect(String ip, int port) throws IOException { + socket = new Websocket("ws://" + ip + ":" + Vars.webPort); + socket.addListener(new WebsocketListener() { + public void onMessage(byte[] bytes) { + try { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + byte id = buffer.get(); + if(id == -2){ + //this is a framework message... do nothing yet? + }else { + Class type = Registrator.getByID(id); + Packet packet = (Packet) ClassReflection.newInstance(type); + packet.read(buffer); + Net.handleClientReceived(packet); + } + }catch (ReflectionException e){ + throw new RuntimeException(e); + } + } + + @Override + public void onClose() { + Disconnect disconnect = new Disconnect(); + Net.handleClientReceived(disconnect); + } + + @Override + public void onMessage(String msg) { + onMessage(Base64Coder.decode(msg)); + } + + @Override + public void onOpen() { + Connect connect = new Connect(); + Net.handleClientReceived(connect); + } + }); + socket.open(); + } + + @Override + public void send(Object object, SendMode mode) { + if(!(object instanceof Packet)) throw new RuntimeException("All sent objects must be packets!"); + Packet p = (Packet)object; + buffer.position(0); + buffer.put(Registrator.getID(object.getClass())); + p.write(buffer); + int pos = buffer.position(); + buffer.position(0); + byte[] out = new byte[pos]; + buffer.get(out); + String string = new String(Base64Coder.encode(out)); + socket.send(string); + } + + @Override + public void updatePing() { + + } + + @Override + public int getPing() { + return 0; + } + + @Override + public void disconnect() { + socket.close(); + } + + @Override + public Array discover() { + return new Array<>(); + } + + @Override + public void pingHost(String address, int port, Consumer valid, Consumer failed) { + failed.accept(new IOException()); + Websocket socket = new Websocket("ws://" + address + ":" + Vars.webPort); + final boolean[] accepted = {false}; + socket.addListener(new WebsocketListener() { + @Override + public void onClose() { + if(!accepted[0]) failed.accept(new IOException("Failed to connect to host.")); + } + + @Override + public void onMessage(String msg) { + String[] text = msg.split("\\|"); + Host host = new Host(text[1], address, Strings.parseInt(text[0])); + valid.accept(host); + accepted[0] = true; + socket.close(); + } + + @Override + public void onOpen() { + socket.send("_ping_"); + } + }); + socket.open(); + Timers.runTask(60f*5, () -> { + if(!accepted[0]){ + failed.accept(new IOException("Failed to connect to host.")); + socket.close(); + } + }); + } + + @Override + public void register(Class... types) { } + + @Override + public void dispose() { + socket.close(); + } +} diff --git a/kryonet/src/io/anuke/kryonet/ByteSerializer.java b/kryonet/src/io/anuke/kryonet/ByteSerializer.java new file mode 100644 index 0000000000..257ffce93c --- /dev/null +++ b/kryonet/src/io/anuke/kryonet/ByteSerializer.java @@ -0,0 +1,61 @@ +package io.anuke.kryonet; + +import com.badlogic.gdx.utils.reflect.ClassReflection; +import com.badlogic.gdx.utils.reflect.ReflectionException; +import com.esotericsoftware.kryonet.FrameworkMessage; +import com.esotericsoftware.kryonet.serialization.Serialization; +import io.anuke.mindustry.net.Packet; +import io.anuke.mindustry.net.Registrator; + +import java.nio.ByteBuffer; + +public class ByteSerializer implements Serialization { + + @Override + public void write(ByteBuffer byteBuffer, Object o) { + if(o instanceof FrameworkMessage){ + byteBuffer.put((byte)-2); //code for framework message + FrameworkSerializer.write(byteBuffer, (FrameworkMessage)o); + }else { + if (!(o instanceof Packet)) + throw new RuntimeException("All sent objects must implement be Packets! Class: " + o.getClass()); + byte id = Registrator.getID(o.getClass()); + if (id == -1) + throw new RuntimeException("Unregistered class: " + ClassReflection.getSimpleName(o.getClass())); + byteBuffer.put(id); + ((Packet) o).write(byteBuffer); + } + } + + @Override + public Object read(ByteBuffer byteBuffer) { + try { + byte id = byteBuffer.get(); + if(id == -2){ + return FrameworkSerializer.read(byteBuffer); + }else { + Class type = Registrator.getByID(id); + Packet packet = (Packet) ClassReflection.newInstance(type); + packet.read(byteBuffer); + return packet; + } + }catch (ReflectionException e){ + throw new RuntimeException(e); + } + } + + @Override + public int getLengthLength() { + return 2; + } + + @Override + public void writeLength(ByteBuffer byteBuffer, int i) { + byteBuffer.putShort((short)i); + } + + @Override + public int readLength(ByteBuffer byteBuffer) { + return byteBuffer.getShort(); + } +} diff --git a/kryonet/src/io/anuke/kryonet/FrameworkSerializer.java b/kryonet/src/io/anuke/kryonet/FrameworkSerializer.java new file mode 100644 index 0000000000..ac3d8ae65d --- /dev/null +++ b/kryonet/src/io/anuke/kryonet/FrameworkSerializer.java @@ -0,0 +1,69 @@ +package io.anuke.kryonet; + +import com.esotericsoftware.kryonet.FrameworkMessage; +import com.esotericsoftware.kryonet.FrameworkMessage.*; + +import java.nio.ByteBuffer; + +public class FrameworkSerializer { + + public static void write(ByteBuffer buffer, FrameworkMessage message){ + if(message instanceof Ping){ + Ping p = (Ping)message; + + buffer.put((byte)0); + buffer.putInt(p.id); + buffer.put(p.isReply ? 1 : (byte)0); + }else if(message instanceof DiscoverHost){ + DiscoverHost p = (DiscoverHost)message; + + buffer.put((byte)1); + }else if(message instanceof KeepAlive){ + KeepAlive p = (KeepAlive)message; + + buffer.put((byte)2); + }else if(message instanceof RegisterUDP){ + RegisterUDP p = (RegisterUDP)message; + + buffer.put((byte)3); + buffer.putInt(p.connectionID); + }else if(message instanceof RegisterTCP){ + RegisterTCP p = (RegisterTCP)message; + + buffer.put((byte)4); + buffer.putInt(p.connectionID); + } + } + + public static FrameworkMessage read(ByteBuffer buffer){ + byte id = buffer.get(); + + if(id == 0){ + Ping p = new Ping(); + p.id = buffer.getInt(); + p.isReply = buffer.get() == 1; + + return p; + }else if(id == 1){ + DiscoverHost p = new DiscoverHost(); + + return p; + }else if(id == 2){ + KeepAlive p = new KeepAlive(); + + return p; + }else if(id == 3){ + RegisterUDP p = new RegisterUDP(); + p.connectionID = buffer.getInt(); + + return p; + }else if(id == 4){ + RegisterTCP p = new RegisterTCP(); + p.connectionID = buffer.getInt(); + + return p; + }else{ + throw new RuntimeException("Unknown framework message!"); + } + } +} diff --git a/kryonet/src/io/anuke/kryonet/JavaWebsocketClient.java b/kryonet/src/io/anuke/kryonet/JavaWebsocketClient.java new file mode 100644 index 0000000000..b053cdf144 --- /dev/null +++ b/kryonet/src/io/anuke/kryonet/JavaWebsocketClient.java @@ -0,0 +1,147 @@ +package io.anuke.kryonet; + +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Base64Coder; +import com.badlogic.gdx.utils.reflect.ClassReflection; +import io.anuke.mindustry.Vars; +import io.anuke.mindustry.net.Host; +import io.anuke.mindustry.net.Net; +import io.anuke.mindustry.net.Net.ClientProvider; +import io.anuke.mindustry.net.Net.SendMode; +import io.anuke.mindustry.net.Packet; +import io.anuke.mindustry.net.Packets.Connect; +import io.anuke.mindustry.net.Packets.Disconnect; +import io.anuke.mindustry.net.Registrator; +import io.anuke.ucore.UCore; +import io.anuke.ucore.function.Consumer; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.handshake.ServerHandshake; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +public class JavaWebsocketClient implements ClientProvider { + WebSocketClient socket; + ByteBuffer buffer = ByteBuffer.allocate(1024); + boolean debug = false; + + @Override + public void connect(String ip, int port) throws IOException { + try { + URI i = new URI("ws://" + ip + ":" + Vars.webPort); + UCore.log("Connecting: " + i); + socket = new WebSocketClient(i, new Draft_6455(), null, 5000) { + Thread thread; + + @Override + public void connect() { + if(thread != null ) + throw new IllegalStateException( "WebSocketClient objects are not reuseable" ); + thread = new Thread(this); + thread.setDaemon(true); + thread.start(); + } + + @Override + public void onOpen(ServerHandshake handshakedata) { + UCore.log("Connected!"); + Connect connect = new Connect(); + Net.handleClientReceived(connect); + } + + @Override + public void onMessage(String message) { + if(debug) UCore.log("Got message: " + message); + try { + byte[] bytes = Base64Coder.decode(message); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + byte id = buffer.get(); + if (id == -2) { + //this is a framework message... do nothing yet? + } else { + Class type = Registrator.getByID(id); + if(debug) UCore.log("Got class ID: " + type); + Packet packet = (Packet) ClassReflection.newInstance(type); + packet.read(buffer); + Net.handleClientReceived(packet); + } + } catch (Exception e) { + e.printStackTrace(); + //throw new RuntimeException(e); + } + } + + @Override + public void onClose(int code, String reason, boolean remote) { + if(debug) UCore.log("Closed."); + Disconnect disconnect = new Disconnect(); + Net.handleClientReceived(disconnect); + } + + @Override + public void onError(Exception ex) { + onClose(0, null, true); + ex.printStackTrace(); + } + }; + socket.connect(); + }catch (URISyntaxException e){ + throw new IOException(e); + } + } + + @Override + public void send(Object object, SendMode mode) { + if(!(object instanceof Packet)) throw new RuntimeException("All sent objects must be packets!"); + Packet p = (Packet)object; + buffer.position(0); + buffer.put(Registrator.getID(object.getClass())); + p.write(buffer); + int pos = buffer.position(); + buffer.position(0); + byte[] out = new byte[pos]; + buffer.get(out); + String string = new String(Base64Coder.encode(out)); + if(debug) UCore.log("Sending string: " + string); + socket.send(string); + } + + @Override + public void updatePing() { + + } + + @Override + public int getPing() { + return 0; + } + + @Override + public void disconnect() { + socket.close(); + } + + @Override + public Array discover() { + return new Array<>(); + } + + @Override + public void pingHost(String address, int port, Consumer valid, Consumer failed) { + failed.accept(new IOException()); + } + + @Override + public void register(Class... types) { } + + @Override + public void dispose() { + if(socket != null) socket.close(); + for(Thread thread : Thread.getAllStackTraces().keySet()){ + if(thread.getName().equals("WebsocketWriteThread")) thread.interrupt(); + } + } +} diff --git a/kryonet/src/io/anuke/kryonet/KryoClient.java b/kryonet/src/io/anuke/kryonet/KryoClient.java index fc358225ed..3a4bc0dd50 100644 --- a/kryonet/src/io/anuke/kryonet/KryoClient.java +++ b/kryonet/src/io/anuke/kryonet/KryoClient.java @@ -52,7 +52,7 @@ public class KryoClient implements ClientProvider{ } }; - client = new Client(8192, 2048); + client = new Client(8192, 2048, connection -> new ByteSerializer()); client.setDiscoveryHandler(handler); Listener listener = new Listener(){ @@ -211,12 +211,7 @@ public class KryoClient implements ClientProvider{ } @Override - public void register(Class... types) { - for(Class c : types){ - client.getKryo().register(c); - } - KryoRegistrator.register(client.getKryo()); - } + public void register(Class... types) { } @Override public void dispose(){ diff --git a/kryonet/src/io/anuke/kryonet/KryoServer.java b/kryonet/src/io/anuke/kryonet/KryoServer.java index e312177724..2ee5cf6266 100644 --- a/kryonet/src/io/anuke/kryonet/KryoServer.java +++ b/kryonet/src/io/anuke/kryonet/KryoServer.java @@ -1,16 +1,19 @@ package io.anuke.kryonet; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.utils.IntArray; +import com.badlogic.gdx.utils.Array; +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.mindustry.Vars; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.Net.SendMode; import io.anuke.mindustry.net.Net.ServerProvider; +import io.anuke.mindustry.net.NetConnection; import io.anuke.mindustry.net.Packets.Connect; import io.anuke.mindustry.net.Packets.Disconnect; import io.anuke.mindustry.net.Packets.KickPacket; @@ -21,16 +24,30 @@ import io.anuke.mindustry.net.Streamable.StreamBegin; import io.anuke.mindustry.net.Streamable.StreamChunk; import io.anuke.ucore.UCore; import io.anuke.ucore.core.Timers; +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; import java.io.IOException; +import java.net.BindException; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.CopyOnWriteArrayList; public class KryoServer implements ServerProvider { - Server server; - IntArray connections = new IntArray(); + final boolean debug = false; + final Server server; + final ByteSerializer serializer = new ByteSerializer(); + final ByteBuffer buffer = ByteBuffer.allocate(4096); + final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); + final Array array = new Array<>(); + SocketServer webServer; + + int lastconnection = 0; public KryoServer(){ - server = new Server(4096*2, 2048); //TODO tweak + server = new Server(4096*2, 2048, connection -> new ByteSerializer()); //TODO tweak server.setDiscoveryHandler((datagramChannel, fromAddress) -> { ByteBuffer buffer = KryoRegistrator.writeServerData(); UCore.log("Replying to discover request with buffer of size " + buffer.capacity()); @@ -43,13 +60,15 @@ public class KryoServer implements ServerProvider { @Override public void connected (Connection connection) { + KryoConnection kn = new KryoConnection(lastconnection ++, connection.getRemoteAddressTCP().toString(), connection); + Connect c = new Connect(); - c.id = connection.getID(); + c.id = kn.id; c.addressTCP = connection.getRemoteAddressTCP().toString(); try { - Net.handleServerReceived(c, c.id); - connections.add(c.id); + Net.handleServerReceived(c, kn.id); + connections.add(kn); }catch (Exception e){ Gdx.app.postRunnable(() -> {throw new RuntimeException(e);}); } @@ -57,10 +76,12 @@ public class KryoServer implements ServerProvider { @Override public void disconnected (Connection connection) { - connections.removeValue(connection.getID()); + KryoConnection k = getByKryoID(connection.getID()); + if(k == null) return; + connections.remove(k); Disconnect c = new Disconnect(); - c.id = connection.getID(); + c.id = k.id; try{ Net.handleServerReceived(c, c.id); @@ -71,14 +92,13 @@ public class KryoServer implements ServerProvider { @Override public void received (Connection connection, Object object) { - if(object instanceof FrameworkMessage) return; + KryoConnection k = getByKryoID(connection.getID()); + if(object instanceof FrameworkMessage || k == null) return; try{ - Net.handleServerReceived(object, connection.getID()); + Net.handleServerReceived(object, k.id); }catch (Exception e){ - //...do absolutely nothing. e.printStackTrace(); - //Gdx.app.postRunnable(() -> {throw new RuntimeException(e);}); } } }; @@ -93,33 +113,31 @@ public class KryoServer implements ServerProvider { } @Override - public IntArray getConnections() { - return connections; + public Array getConnections() { + array.clear(); + for(KryoConnection c : connections){ + array.add(c); + } + return array; } @Override public void kick(int connection) { - Connection conn = getByID(connection); - - if(conn == null){ - connections.removeValue(connection); - return; - } + KryoConnection con = getByID(connection); KickPacket p = new KickPacket(); - p.reason = (byte)KickReason.kick.ordinal(); + p.reason = KickReason.kick; - conn.sendTCP(p); - Timers.runTask(1f, () -> { - if(conn.isConnected()){ - conn.close(); - } - }); + con.send(p, SendMode.tcp); + Timers.runTask(1f, con::close); } @Override public void host(int port) throws IOException { + lastconnection = 0; server.bind(port, port); + webServer = new SocketServer(Vars.webPort); + webServer.start(); Thread thread = new Thread(() -> { try{ @@ -136,80 +154,116 @@ public class KryoServer implements ServerProvider { public void close() { UCore.setPrivate(server, "shutdown", true); - new Thread(() -> server.close()).run(); + Thread thread = new Thread(() ->{ + try { + server.close(); + if(webServer != null) webServer.stop(1); //please die, right now + //kill them all + for(Thread worker : Thread.getAllStackTraces().keySet()){ + if(worker.getName().contains("WebSocketWorker")){ + worker.interrupt(); + } + } + }catch (Exception e){ + Gdx.app.postRunnable(() -> {throw new RuntimeException(e);}); + } + }); + thread.setDaemon(true); + thread.start(); } @Override public void sendStream(int id, Streamable stream) { - Connection connection = getByID(id); + KryoConnection connection = getByID(id); if(connection == null) return; + try { - connection.addListener(new InputStreamSender(stream.stream, 512) { - int id; + if (connection.connection != null) { - protected void start () { - //send an object so the receiving side knows how to handle the following chunks + connection.connection.addListener(new InputStreamSender(stream.stream, 512) { + int id; + + protected void start() { + //send an object so the receiving side knows how to handle the following chunks + StreamBegin begin = new StreamBegin(); + begin.total = stream.stream.available(); + begin.type = stream.getClass(); + connection.connection.sendTCP(begin); + id = begin.id; + } + + protected Object next(byte[] bytes) { + StreamChunk chunk = new StreamChunk(); + chunk.id = id; + chunk.data = bytes; + return chunk; //wrap the byte[] with an object so the receiving side knows how to handle it. + } + }); + } else { + int cid; StreamBegin begin = new StreamBegin(); begin.total = stream.stream.available(); begin.type = stream.getClass(); - connection.sendTCP(begin); - id = begin.id; - } + connection.send(begin, SendMode.tcp); + cid = begin.id; - protected Object next (byte[] bytes) { - StreamChunk chunk = new StreamChunk(); - chunk.id = id; - chunk.data = bytes; - return chunk; //wrap the byte[] with an object so the receiving side knows how to handle it. + while (stream.stream.available() > 0) { + byte[] bytes = new byte[Math.min(512, stream.stream.available())]; + stream.stream.read(bytes); + + StreamChunk chunk = new StreamChunk(); + chunk.id = cid; + chunk.data = bytes; + connection.send(chunk, SendMode.tcp); + } } - }); + }catch (IOException e){ + throw new RuntimeException(e); + } } @Override public void send(Object object, SendMode mode) { - if(mode == SendMode.tcp){ - server.sendToAllTCP(object); - }else{ - server.sendToAllUDP(object); + for(int i = 0; i < connections.size(); i ++){ + connections.get(i).send(object, mode); } } @Override public void sendTo(int id, Object object, SendMode mode) { - if(mode == SendMode.tcp){ - server.sendToTCP(id, object); - }else{ - server.sendToUDP(id, object); - } + NetConnection conn = getByID(id); + conn.send(object, mode); } @Override public void sendExcept(int id, Object object, SendMode mode) { - if(mode == SendMode.tcp){ - server.sendToAllExceptTCP(id, object); - }else{ - server.sendToAllExceptUDP(id, object); + for(int i = 0; i < connections.size(); i ++){ + KryoConnection conn = connections.get(i); + if(conn.id != id) conn.send(object, mode); } } @Override - public int getPingFor(int connection) { - return getByID(connection).getReturnTripTime(); + public int getPingFor(NetConnection con) { + KryoConnection k = (KryoConnection)con; + return k.connection == null ? 0 : k.connection.getReturnTripTime(); } @Override - public void register(Class... types) { - for(Class c : types){ - server.getKryo().register(c); - } - KryoRegistrator.register(server.getKryo()); - } + public void register(Class... types) { } @Override public void dispose(){ try { server.dispose(); - }catch (IOException e){ + if(webServer != null) webServer.stop(1); + //kill them all + for(Thread thread : Thread.getAllStackTraces().keySet()){ + if(thread.getName().contains("WebSocketWorker")){ + thread.interrupt(); + } + } + }catch (Exception e){ throw new RuntimeException(e); } } @@ -218,13 +272,158 @@ public class KryoServer implements ServerProvider { Gdx.app.postRunnable(() -> { throw new RuntimeException(e);}); } - Connection getByID(int id){ - for(Connection con : server.getConnections()){ - if(con.getID() == id){ + KryoConnection getByID(int id){ + for(int i = 0; i < connections.size(); i ++){ + KryoConnection con = connections.get(i); + if(con.id == id){ return con; } } return null; } + + KryoConnection getByKryoID(int id){ + for(int i = 0; i < connections.size(); i ++){ + KryoConnection con = connections.get(i); + if(con.connection != null && con.connection.getID() == id){ + return con; + } + } + + return null; + } + + KryoConnection getBySocket(WebSocket socket){ + for(int i = 0; i < connections.size(); i ++){ + KryoConnection con = connections.get(i); + if(con.socket == socket){ + return con; + } + } + + return null; + } + + class KryoConnection extends NetConnection{ + public final WebSocket socket; + public final Connection connection; + + public KryoConnection(int id, String address, WebSocket socket) { + super(id, address); + this.socket = socket; + this.connection = null; + } + + public KryoConnection(int id, String address, Connection connection) { + super(id, address); + this.socket = null; + this.connection = connection; + } + + @Override + public void send(Object object, SendMode mode){ + if(socket != null){ + try { + synchronized (buffer) { + buffer.position(0); + if(debug) UCore.log("Sending object with ID " + Registrator.getID(object.getClass())); + serializer.write(buffer, object); + int pos = buffer.position(); + buffer.position(0); + byte[] out = new byte[pos]; + buffer.get(out); + String string = new String(Base64Coder.encode(out)); + if(debug) UCore.log("Sending string: " + string); + socket.send(string); + } + }catch (Exception e){ + e.printStackTrace(); + connections.remove(this); + } + }else if (connection != null) { + if (mode == SendMode.tcp) { + connection.sendTCP(object); + } else { + connection.sendUDP(object); + } + } + } + + @Override + public void close(){ + if(socket != null){ + if(socket.isOpen()) socket.close(); + }else if (connection != null) { + if(connection.isConnected()) connection.close(); + } + } + + } + + class SocketServer extends WebSocketServer { + + public SocketServer(int port) { + super(new InetSocketAddress(port)); + } + + @Override + public void onOpen(WebSocket conn, ClientHandshake handshake) { + Connect connect = new Connect(); + connect.addressTCP = conn.getRemoteSocketAddress().toString(); + UCore.log("Websocket connection recieved: " + connect.addressTCP); + KryoConnection kn = new KryoConnection(lastconnection ++, connect.addressTCP, conn); + connections.add(kn); + } + + @Override + public void onClose(WebSocket conn, int code, String reason, boolean remote) { + if (conn == null) return; + KryoConnection k = getBySocket(conn); + if(k == null) return; + Disconnect disconnect = new Disconnect(); + disconnect.id = k.id; + Net.handleServerReceived(disconnect, k.id); + } + + @Override + public void onMessage(WebSocket conn, String message) { + try { + KryoConnection k = getBySocket(conn); + if (k == null) return; + + if(message.equals("_ping_")){ + conn.send(connections.size() + "|" + Vars.player.name); + connections.remove(k); + }else { + if (debug) UCore.log("Got message: " + message); + + byte[] out = Base64Coder.decode(message); + if (debug) UCore.log("Decoded: " + Arrays.toString(out)); + ByteBuffer buffer = ByteBuffer.wrap(out); + Object o = serializer.read(buffer); + Net.handleServerReceived(o, k.id); + } + }catch (Exception e){ + UCore.log("Error reading message!"); + e.printStackTrace(); + } + } + + @Override + public void onError(WebSocket conn, Exception ex) { + UCore.log("WS error:"); + ex.printStackTrace(); + if(ex instanceof BindException){ + Net.closeServer(); + Vars.ui.showError("$text.server.addressinuse"); + } + } + + @Override + public void onStart() { + UCore.log("Web server started."); + } + } + }