diff --git a/README.md b/README.md index d381cd547c..a851d06830 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A pixelated sandbox tower defense game made using [LibGDX](https://libgdx.badlogicgames.com/). Winner of the [GDL Metal Monstrosity Jam](https://itch.io/jam/gdl---metal-monstrosity-jam). _[Issue tracker](https://waffle.io/Anuken/Mindustry)_ -_[TODO list](TODO.md)_ +_[Trello Board](https://trello.com/b/aE2tcUwF/mindustry-40-plans)_ _[Wiki](http://mindustry.wikia.com/wiki/Mindustry_Wiki)_ _[Discord](https://discord.gg/r8BkXNd)_ diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 8118e89982..0000000000 --- a/TODO.md +++ /dev/null @@ -1,86 +0,0 @@ -_Keep in mind that this is just a basic outline of planned features, and will be constantly changing!_ - -### Won't Add -_(These are not planned in the near future at all, and have been suggested before **many** times.)_ -- Texture packs -- Online player profiles -- Player mech on Android -- Modding support -- Game speed increase (fast forward) -- Liquid teleporter -- More teleporter colors/any system that gives it more frequencies (numbers) -- More ore generation (if you're playing on maze, *play any different map, maze has the least resources, and for good reason!*) -- Power wires or conduits - -### Already Suggested -_(not necessarily planned!)_ -- "more blocks" "more turrets" "more content" "more X/Y/Z" -- Building of units (tanks, drones, _soldiers_, doesn't matter) -- Enemy bases, fighting against AI, capture points -- Co-op of any sort -- Campaign, challenge mode -- PvP mode -- Multiple cores, movable cores -- Movable turrets -- Batteries or storage for anything -- Destroy map indestructible blocks -- Customizable world ore generation + seed -- Steam release -- Research system, tech tree, persistent upgrades, upgrades at all -- Missile enemies/turrets/weapons (both homing and non-homing) -- 'Better graphics' -- Enemies dropping resources -- Final objectives/non-endless mode -- Fusion reactor -- Dams, flowing water -- Flying enemies -- Day/night cycle -- Solar panels -- Deflector shields -- Underground blocks -- Configurable outputs/inputs -- Getting items out of the core -- Map sharing/map browser - -### Balance -- Slow down progression slightly -- Better endgame turrets (?) -- Nerf RTG, buff nuclear reactor -- Faster power - -### Misc. QoL -- Minimap -- Underground conduits -- More indicators for core damaged/attacked -- Display playtime in saves -- New liquid conduit system - -### Major Bugs -- Black screen when tabbing out on Android -- Random map reload when playing, leading to a crash (UI cause?) -- Google Payments verify crash -- Google Payments IllegalArgument crash - -### Possible Additions -- Mech body upgrades -- Uranium extractor / uranium->iron converter -- Laser enemies -- Flying enemies that move in formation and spawn from different locations -- Fusion reactor -- Point defense turrets that take down projectiles -- Turrets fueled by lava -- Gas transporation and use -- Better enemy effects and looks -- Homing missile enemies and turrets -- Reflective shield blocks -- Spawn points changed into enemy bases with hostile turrets -- Unit production - -### Optimization -- Optimize enemy + bullet code and check quadtree leaf parameters -- Optimize generator laser distribution, especially finding targets -- Optimize UI -- Check memory usage and GC, profile -- Optimize health bars and enemies in general -- Make drawing of enemies more efficient (don't call `flush()`?) - diff --git a/android/src/io/anuke/mindustry/AndroidLauncher.java b/android/src/io/anuke/mindustry/AndroidLauncher.java index 8870d5b567..a3a9a3fd41 100644 --- a/android/src/io/anuke/mindustry/AndroidLauncher.java +++ b/android/src/io/anuke/mindustry/AndroidLauncher.java @@ -6,15 +6,18 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; +import android.provider.Settings.Secure; import android.telephony.TelephonyManager; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; +import com.badlogic.gdx.utils.Base64Coder; import io.anuke.kryonet.DefaultThreadImpl; import io.anuke.kryonet.KryoClient; import io.anuke.kryonet.KryoServer; import io.anuke.mindustry.core.ThreadHandler.ThreadProvider; import io.anuke.mindustry.io.Platform; import io.anuke.mindustry.net.Net; +import io.anuke.ucore.core.Settings; import io.anuke.ucore.scene.ui.TextField; import io.anuke.ucore.scene.ui.layout.Unit; @@ -23,6 +26,7 @@ import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.Random; public class AndroidLauncher extends AndroidApplication{ boolean doubleScaleTablets = true; @@ -97,6 +101,40 @@ public class AndroidLauncher extends AndroidApplication{ public boolean isDebug() { return false; } + + @Override + public byte[] getUUID() { + try { + String s = Secure.getString(getContext().getContentResolver(), + Secure.ANDROID_ID); + + if(s == null){ + Settings.defaults("uuid", ""); + + String uuid = Settings.getString("uuid"); + if(uuid.isEmpty()){ + byte[] result = new byte[8]; + new Random().nextBytes(result); + uuid = new String(Base64Coder.encode(result)); + Settings.putString("uuid", uuid); + Settings.save(); + return result; + } + return Base64Coder.decode(uuid); + } + + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + + return data; + }catch (Exception e){ + return null; + } + } }; if(doubleScaleTablets && isTablet(this.getContext())){ diff --git a/build.gradle b/build.gradle index 2362e21495..70d7e461d8 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ allprojects { appName = 'Mindustry' gdxVersion = '1.9.8' aiVersion = '1.8.1' - uCoreVersion = '7f46315' + uCoreVersion = '21f04f8' getVersionString = { String buildVersion = getBuildVersion() diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 67ed2035c4..b93cd3c82c 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -41,12 +41,15 @@ text.server.friendlyfire=Friendly Fire text.trace=Trace Player text.trace.playername=Player name: [accent]{0} text.trace.ip=IP: [accent]{0} +text.trace.id=Unique ID: [accent]{0} +text.trace.android=Android Client: [accent]{0} text.trace.modclient=Custom Client: [accent]{0} text.trace.totalblocksbroken=Total blocks broken: [accent]{0} text.trace.structureblocksbroken=Structure blocks broken: [accent]{0} text.trace.lastblockbroken=Last block broken: [accent]{0} text.trace.totalblocksplaced=Total blocks placed: [accent]{0} text.trace.lastblockplaced=Last block placed: [accent]{0} +text.invalidid=Invalid client ID! Submit a bug report. text.server.bans=Bans text.server.bans.none=No banned players found! text.server.admins=Admins @@ -55,6 +58,10 @@ text.server.add=Add Server text.server.delete=Are you sure you want to delete this server? text.server.hostname=Host: {0} text.server.edit=Edit Server +text.server.outdated=[crimson]Outdated Server![] +text.server.outdated.client=[crimson]Outdated Client![] +text.server.version=[lightgray]Version: {0} +text.server.custombuild=[yellow]Custom Build text.confirmban=Are you sure you want to ban this player? text.confirmunban=Are you sure you want to unban this player? text.confirmadmin=Are you sure you want to make this player an admin? diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index 28dc762143..97cb4668c4 100644 --- a/core/src/io/anuke/mindustry/core/NetClient.java +++ b/core/src/io/anuke/mindustry/core/NetClient.java @@ -1,6 +1,5 @@ package io.anuke.mindustry.core; -import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.utils.IntMap; import com.badlogic.gdx.utils.IntSet; @@ -67,11 +66,20 @@ public class NetClient extends Module { c.name = player.name; c.android = android; c.color = Color.rgba8888(player.color); + c.uuid = Platform.instance.getUUID(); + + if(c.uuid == null){ + ui.showError("$text.invalidid"); + ui.loadfrag.hide(); + disconnectQuietly(); + return; + } + Net.send(c, SendMode.tcp); Timers.runTask(dataTimeout, () -> { if (!gotData) { - Gdx.app.error("Mindustry", "Failed to load data!"); + Log.err("Failed to load data!"); ui.loadfrag.hide(); Net.disconnect(); } diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index a10963d50f..ee61924be6 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -51,18 +51,27 @@ public class NetServer extends Module{ Events.on(GameOverEvent.class, () -> weapons.clear()); Net.handleServer(Connect.class, (id, connect) -> { - if(admins.isBanned(connect.addressTCP)){ + if(admins.isIPBanned(connect.addressTCP)){ Net.kickConnection(id, KickReason.banned); } }); Net.handleServer(ConnectPacket.class, (id, packet) -> { + String uuid = new String(Base64Coder.encode(packet.uuid)); if(Net.getConnection(id) == null || - admins.isBanned(Net.getConnection(id).address)) return; + admins.isIPBanned(Net.getConnection(id).address)) return; + + if(admins.isIDBanned(uuid)){ + Net.kickConnection(id, KickReason.banned); + return; + } String ip = Net.getConnection(id).address; admins.setKnownName(ip, packet.name); + admins.setKnownIP(uuid, ip); + admins.getTrace(ip).uuid = uuid; + admins.getTrace(ip).android = packet.android; if(packet.version != Version.build && Version.build != -1 && packet.version != -1){ Net.kickConnection(id, packet.version > Version.build ? KickReason.serverOutdated : KickReason.clientOutdated); @@ -270,7 +279,7 @@ public class NetServer extends Module{ String ip = Net.getConnection(other.clientid).address; if(packet.action == AdminAction.ban){ - admins.banPlayer(ip); + admins.banPlayerIP(ip); Net.kickConnection(other.clientid, KickReason.banned); Log.info("&lc{0} has banned {1}.", player.name, other.name); }else if(packet.action == AdminAction.kick){ diff --git a/core/src/io/anuke/mindustry/entities/Bullet.java b/core/src/io/anuke/mindustry/entities/Bullet.java index 15a0243d03..279993b4c4 100644 --- a/core/src/io/anuke/mindustry/entities/Bullet.java +++ b/core/src/io/anuke/mindustry/entities/Bullet.java @@ -4,7 +4,6 @@ import io.anuke.mindustry.entities.enemies.Enemy; import io.anuke.mindustry.world.Tile; import io.anuke.ucore.entities.BulletEntity; import io.anuke.ucore.entities.Entity; -import io.anuke.ucore.entities.SolidEntity; import io.anuke.ucore.util.Timer; import static io.anuke.mindustry.Vars.*; @@ -65,11 +64,6 @@ public class Bullet extends BulletEntity{ } } - @Override - public boolean collides(SolidEntity other){ - return (other instanceof Player) == (owner instanceof Enemy); - } - @Override public int getDamage(){ return damage == -1 ? type.damage : damage; diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index 858716e6b6..4292aea7f4 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -23,10 +23,10 @@ public class DesktopInput extends InputHandler{ private boolean beganBreak; private boolean rotated = false, rotatedAlt, zoomed; - @Override public float getCursorEndX(){ return endx; } - @Override public float getCursorEndY(){ return endy; } - @Override public float getCursorX(){ return (int)(Graphics.screen(mousex, mousey).x + 2); } - @Override public float getCursorY(){ return (int)(Gdx.graphics.getHeight() - 1 - Graphics.screen(mousex, mousey).y); } + @Override public float getCursorEndX(){ return select() ? getCursorX() : endx; } + @Override public float getCursorEndY(){ return select() ? getCursorY() : endy; } + @Override public float getCursorX(){ return (int)(Graphics.screen(mousex, mousey).x); } + @Override public float getCursorY(){ return (int)(Gdx.graphics.getHeight() - Graphics.screen(mousex, mousey).y); } @Override public boolean drawPlace(){ return !beganBreak; } @Override @@ -156,6 +156,10 @@ public class DesktopInput extends InputHandler{ } + boolean select(){ + return !Inputs.keyDown("select") && !Inputs.keyRelease("select"); + } + public int tilex(){ return (recipe != null && recipe.result.isMultiblock() && recipe.result.size % 2 == 0) ? diff --git a/core/src/io/anuke/mindustry/io/Platform.java b/core/src/io/anuke/mindustry/io/Platform.java index 8cab6c603e..1f361c8895 100644 --- a/core/src/io/anuke/mindustry/io/Platform.java +++ b/core/src/io/anuke/mindustry/io/Platform.java @@ -30,6 +30,8 @@ public abstract class Platform { return true; } public boolean isDebug(){return false;} + /**Must be 8 bytes in length.*/ + public byte[] getUUID(){return null;} public ThreadProvider getThreadProvider(){ return new ThreadProvider() { @Override public boolean isOnThread() {return true;} diff --git a/core/src/io/anuke/mindustry/net/Administration.java b/core/src/io/anuke/mindustry/net/Administration.java index 27ebe8f1d5..a37db3aefd 100644 --- a/core/src/io/anuke/mindustry/net/Administration.java +++ b/core/src/io/anuke/mindustry/net/Administration.java @@ -7,16 +7,20 @@ import io.anuke.ucore.core.Settings; public class Administration { private Json json = new Json(); - private Array bannedIPS = new Array<>(); + private Array bannedIPs = new Array<>(); + private Array bannedIDs = new Array<>(); private Array admins = new Array<>(); - private ObjectMap known = new ObjectMap<>(); + private ObjectMap ipNames = new ObjectMap<>(); + private ObjectMap idIPs = new ObjectMap<>(); private ObjectMap traces = new ObjectMap<>(); public Administration(){ Settings.defaultList( "bans", "{}", + "bannedIDs", "{}", "admins", "{}", - "knownIPs", "{}" + "knownIPs", "{}", + "knownIDs", "{}" ); load(); @@ -34,35 +38,81 @@ public class Administration { /**Sets last known name for an IP.*/ public void setKnownName(String ip, String name){ - known.put(ip, name); + ipNames.put(ip, name); + saveKnown(); + } + + /**Sets last known UUID for an IP.*/ + public void setKnownIP(String id, String ip){ + idIPs.put(id, ip); saveKnown(); } /**Returns the last known name for an IP. Returns 'unknown' if this IP has an unknown username.*/ public String getLastName(String ip){ - return known.get(ip, "unknown"); + return ipNames.get(ip, "unknown"); + } + + /**Returns the last known IP for a UUID. Returns 'unknown' if this IP has an unknown IP.*/ + public String getLastIP(String id){ + return idIPs.get(id, "unknown"); + } + + /**Return the last known device ID associated with an IP. Returns 'unknown' if this IP has an unknown device.*/ + public String getLastID(String ip){ + for(String id : idIPs.keys()){ + if(idIPs.get(id).equals(ip)){ + return id; + } + } + return "unknown"; } /**Returns list of banned IPs.*/ public Array getBanned(){ - return bannedIPS; + return bannedIPs; + } + + /**Returns list of banned IDs.*/ + public Array getBannedIDs(){ + return bannedIDs; } /**Bans a player by IP; returns whether this player was already banned.*/ - public boolean banPlayer(String ip){ - if(bannedIPS.contains(ip, false)) + public boolean banPlayerIP(String ip){ + if(bannedIPs.contains(ip, false)) return false; - bannedIPS.add(ip); + bannedIPs.add(ip); + saveBans(); + + return true; + } + + /**Bans a player by UUID.*/ + public boolean banPlayerID(String id){ + if(bannedIDs.contains(id, false)) + return false; + bannedIDs.add(id); saveBans(); return true; } /**Unbans a player by IP; returns whether this player was banned in the first place..*/ - public boolean unbanPlayer(String ip){ - if(!bannedIPS.contains(ip, false)) + public boolean unbanPlayerIP(String ip){ + if(!bannedIPs.contains(ip, false)) return false; - bannedIPS.removeValue(ip, false); + bannedIPs.removeValue(ip, false); + saveBans(); + + return true; + } + + /**Unbans a player by IP; returns whether this player was banned in the first place..*/ + public boolean unbanPlayerID(String ip){ + if(!bannedIDs.contains(ip, false)) + return false; + bannedIDs.removeValue(ip, false); saveBans(); return true; @@ -93,8 +143,12 @@ public class Administration { return true; } - public boolean isBanned(String ip){ - return bannedIPS.contains(ip, false); + public boolean isIPBanned(String ip){ + return bannedIPs.contains(ip, false); + } + + public boolean isIDBanned(String uuid){ + return bannedIDs.contains(uuid, false); } public boolean isAdmin(String ip){ @@ -102,12 +156,14 @@ public class Administration { } private void saveKnown(){ - Settings.putString("knownIPs", json.toJson(known)); + Settings.putString("knownIPs", json.toJson(ipNames)); + Settings.putString("knownIDs", json.toJson(idIPs)); Settings.save(); } private void saveBans(){ - Settings.putString("bans", json.toJson(bannedIPS)); + Settings.putString("bans", json.toJson(bannedIPs)); + Settings.putString("bannedIDs", json.toJson(bannedIDs)); Settings.save(); } @@ -117,9 +173,11 @@ public class Administration { } private void load(){ - bannedIPS = json.fromJson(Array.class, Settings.getString("bans")); + bannedIPs = json.fromJson(Array.class, Settings.getString("bans")); + bannedIDs = json.fromJson(Array.class, Settings.getString("bannedIDs")); admins = json.fromJson(Array.class, Settings.getString("admins")); - known = json.fromJson(ObjectMap.class, Settings.getString("knownIPs")); + ipNames = json.fromJson(ObjectMap.class, Settings.getString("knownIPs")); + idIPs = json.fromJson(ObjectMap.class, Settings.getString("knownIDs")); } } diff --git a/core/src/io/anuke/mindustry/net/Host.java b/core/src/io/anuke/mindustry/net/Host.java index 415b90913f..631f74a726 100644 --- a/core/src/io/anuke/mindustry/net/Host.java +++ b/core/src/io/anuke/mindustry/net/Host.java @@ -6,12 +6,14 @@ public class Host { public final String mapname; public final int wave; public final int players; + public final int version; - public Host(String name, String address, String mapname, int wave, int players){ + public Host(String name, String address, String mapname, int wave, int players, int version){ this.name = name; this.address = address; this.players = players; this.mapname = mapname; this.wave = wave; + this.version = version; } } diff --git a/core/src/io/anuke/mindustry/net/NetworkIO.java b/core/src/io/anuke/mindustry/net/NetworkIO.java index c014034d34..8196fbc6ab 100644 --- a/core/src/io/anuke/mindustry/net/NetworkIO.java +++ b/core/src/io/anuke/mindustry/net/NetworkIO.java @@ -6,6 +6,7 @@ import com.badlogic.gdx.utils.ByteArray; import com.badlogic.gdx.utils.TimeUtils; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.game.GameMode; +import io.anuke.mindustry.io.Version; import io.anuke.mindustry.resource.Upgrade; import io.anuke.mindustry.resource.Weapon; import io.anuke.mindustry.world.*; @@ -16,6 +17,7 @@ import io.anuke.ucore.core.Timers; import io.anuke.ucore.entities.Entities; import java.io.*; +import java.nio.ByteBuffer; import static io.anuke.mindustry.Vars.*; @@ -342,4 +344,46 @@ public class NetworkIO { throw new RuntimeException(e); } } + + public static ByteBuffer writeServerData(){ + int maxlen = 32; + + String host = (headless ? "Server" : player.name); + String map = world.getMap().name; + + host = host.substring(0, Math.min(host.length(), maxlen)); + map = map.substring(0, Math.min(map.length(), maxlen)); + + ByteBuffer buffer = ByteBuffer.allocate(128); + + buffer.put((byte)host.getBytes().length); + buffer.put(host.getBytes()); + + buffer.put((byte)map.getBytes().length); + buffer.put(map.getBytes()); + + buffer.putInt(playerGroup.size()); + buffer.putInt(state.wave); + buffer.putInt(Version.build); + return buffer; + } + + public static Host readServerData(String hostAddress, ByteBuffer buffer){ + byte hlength = buffer.get(); + byte[] hb = new byte[hlength]; + buffer.get(hb); + + byte mlength = buffer.get(); + byte[] mb = new byte[mlength]; + buffer.get(mb); + + String host = new String(hb); + String map = new String(mb); + + int players = buffer.getInt(); + int wave = buffer.getInt(); + int version = buffer.getInt(); + + return new Host(host, hostAddress, map, wave, players, version); + } } diff --git a/core/src/io/anuke/mindustry/net/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index 526f0d5fe2..4a7e209312 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -1,5 +1,6 @@ package io.anuke.mindustry.net; +import com.badlogic.gdx.utils.Base64Coder; import com.badlogic.gdx.utils.reflect.ClassReflection; import com.badlogic.gdx.utils.reflect.ReflectionException; import io.anuke.mindustry.entities.Player; @@ -56,6 +57,7 @@ public class Packets { public String name; public boolean android; public int color; + public byte[] uuid; @Override public void write(ByteBuffer buffer) { @@ -64,6 +66,7 @@ public class Packets { buffer.put(name.getBytes()); buffer.put(android ? (byte)1 : 0); buffer.putInt(color); + buffer.put(uuid); } @Override @@ -75,6 +78,8 @@ public class Packets { name = new String(bytes); android = buffer.get() == 1; color = buffer.getInt(); + uuid = new byte[8]; + buffer.get(uuid); } } @@ -569,6 +574,7 @@ public class Packets { buffer.putShort((short)info.ip.getBytes().length); buffer.put(info.ip.getBytes()); buffer.put(info.modclient ? (byte)1 : 0); + buffer.put(info.android ? (byte)1 : 0); buffer.putInt(info.totalBlocksBroken); buffer.putInt(info.structureBlocksBroken); @@ -576,6 +582,7 @@ public class Packets { buffer.putInt(info.totalBlocksPlaced); buffer.putInt(info.lastBlockPlaced.id); + buffer.put(Base64Coder.decode(info.uuid)); } @Override @@ -589,11 +596,16 @@ public class Packets { info.playerid = id; info.modclient = buffer.get() == 1; + info.android = buffer.get() == 1; info.totalBlocksBroken = buffer.getInt(); info.structureBlocksBroken = buffer.getInt(); info.lastBlockBroken = Block.getByID(buffer.getInt()); info.totalBlocksPlaced = buffer.getInt(); info.lastBlockPlaced = Block.getByID(buffer.getInt()); + byte[] uuid = new byte[8]; + buffer.get(uuid); + + info.uuid = new String(Base64Coder.encode(uuid)); } } } diff --git a/core/src/io/anuke/mindustry/net/ServerDebug.java b/core/src/io/anuke/mindustry/net/ServerDebug.java index fc9709a0b9..a34b05e7c1 100644 --- a/core/src/io/anuke/mindustry/net/ServerDebug.java +++ b/core/src/io/anuke/mindustry/net/ServerDebug.java @@ -6,6 +6,7 @@ import com.badlogic.gdx.utils.TimeUtils; import com.badlogic.gdx.utils.reflect.ClassReflection; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.net.Packets.Disconnect; +import io.anuke.ucore.util.Log; import static io.anuke.mindustry.Vars.playerGroup; @@ -13,12 +14,16 @@ public class ServerDebug { private IntMap, Long>> last = new IntMap<>(); public void handle(int connection, Object packet){ - if(!last.containsKey(connection)) - last.put(connection, new OrderedMap<>()); - if(packet instanceof Disconnect) - last.remove(connection); - else - last.get(connection).put(packet.getClass(), TimeUtils.millis()); + try { + if (!last.containsKey(connection)) + last.put(connection, new OrderedMap<>()); + if (packet instanceof Disconnect) + last.remove(connection); + else + last.get(connection).put(packet.getClass(), TimeUtils.millis()); + }catch (Exception e){ + Log.err(""); + } } public String getOut(){ diff --git a/core/src/io/anuke/mindustry/net/TraceInfo.java b/core/src/io/anuke/mindustry/net/TraceInfo.java index 32aa036bbd..2f37a99efa 100644 --- a/core/src/io/anuke/mindustry/net/TraceInfo.java +++ b/core/src/io/anuke/mindustry/net/TraceInfo.java @@ -7,6 +7,7 @@ public class TraceInfo { public int playerid; public String ip; public boolean modclient; + public boolean android; public int totalBlocksBroken; public int structureBlocksBroken; @@ -15,6 +16,8 @@ public class TraceInfo { public int totalBlocksPlaced; public Block lastBlockPlaced = Blocks.air; + public String uuid; + public TraceInfo(String ip){ this.ip = ip; } diff --git a/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java index eea695c28a..bb6633bef5 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java @@ -41,7 +41,7 @@ public class BansDialog extends FloatingDialog { res.add().growX(); res.addImageButton("icon-cancel", 14*3, () -> { ui.showConfirm("$text.confirm", "$text.confirmunban", () -> { - netServer.admins.unbanPlayer(ip); + netServer.admins.unbanPlayerIP(ip); setup(); }); }).size(h).pad(-14f); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java index e62efde4d9..c86285f8cf 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java @@ -1,9 +1,11 @@ package io.anuke.mindustry.ui.dialogs; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; import io.anuke.mindustry.Vars; import io.anuke.mindustry.io.Platform; +import io.anuke.mindustry.io.Version; import io.anuke.mindustry.net.Host; import io.anuke.mindustry.net.Net; import io.anuke.ucore.core.Settings; @@ -13,6 +15,7 @@ import io.anuke.ucore.scene.ui.Dialog; import io.anuke.ucore.scene.ui.ImageButton; import io.anuke.ucore.scene.ui.ScrollPane; import io.anuke.ucore.scene.ui.TextButton; +import io.anuke.ucore.scene.ui.layout.Cell; import io.anuke.ucore.scene.ui.layout.Table; import io.anuke.ucore.util.Bundles; import io.anuke.ucore.util.Log; @@ -84,7 +87,7 @@ public class JoinDialog extends FloatingDialog { TextButton button = buttons[0] = remote.addButton("[accent]"+server.ip, "clear", () -> { if(!buttons[0].childrenPressed()) connect(server.ip, Vars.port); - }).width(w).height(140f).pad(4f).get(); + }).width(w).height(150f).pad(4f).get(); button.getLabel().setWrap(true); @@ -131,16 +134,37 @@ public class JoinDialog extends FloatingDialog { server.content.label(() -> Bundles.get("text.server.refreshing") + Strings.animated(4, 11, ".")); Net.pingHost(server.ip, server.port, host -> { + String versionString; + + if(host.version == -1) { + versionString = Bundles.format("text.server.version", Bundles.get("text.server.custombuild")); + }else if(host.version == 0){ + versionString = Bundles.get("text.server.outdated"); + }else if(host.version < Version.build && Version.build != -1){ + versionString = Bundles.get("text.server.outdated") + "\n" + + Bundles.format("text.server.version", host.version); + }else if(host.version > Version.build && Version.build != -1){ + versionString = Bundles.get("text.server.outdated.client") + "\n" + + Bundles.format("text.server.version", host.version); + }else{ + versionString = Bundles.format("text.server.version", host.version); + } + server.content.clear(); - server.content.add("[lightgray]" + Bundles.format("text.server.hostname", host.name)).left(); - server.content.row(); - server.content.add("[lightgray]" + (host.players != 1 ? Bundles.format("text.players", host.players) : - Bundles.format("text.players.single", host.players))).left(); - server.content.row(); - server.content.add("[lightgray]" + Bundles.format("text.save.map", host.mapname)).left(); - server.content.row(); - server.content.add("[lightgray]" + Bundles.format("text.save.wave", host.wave)).left(); + server.content.table(t -> { + t.add(versionString).left(); + t.row(); + t.add("[lightgray]" + Bundles.format("text.server.hostname", host.name)).left(); + t.row(); + t.add("[lightgray]" + (host.players != 1 ? Bundles.format("text.players", host.players) : + Bundles.format("text.players.single", host.players))).left(); + t.row(); + t.add("[lightgray]" + Bundles.format("text.save.map", host.mapname) + " / " + Bundles.format("text.save.wave", host.wave)).left(); + }).expand().left().bottom().padLeft(12f).padBottom(8); + + //server.content.add(versionString).top().expandY().top().expandX(); + }, e -> { server.content.clear(); server.content.add("$text.host.invalid"); @@ -161,7 +185,7 @@ public class JoinDialog extends FloatingDialog { hosts.add(remote).growX(); hosts.row(); - hosts.add(local).growX(); + hosts.add(local).width(w); ScrollPane pane = new ScrollPane(hosts, "clear"); pane.setFadeScrollBars(false); @@ -188,15 +212,29 @@ public class JoinDialog extends FloatingDialog { }); }).size(50f, 54f).get(); button.update(() -> button.getStyle().imageUpColor = player.getColor()); - }).width(w).height(70f).pad(4); content().row(); - content().add(pane).width(w).pad(0); + content().add(pane).width(w + 34).pad(0); content().row(); content().addCenteredImageTextButton("$text.server.add", "icon-add", "clear", 14*3, () -> { renaming = null; add.show(); - }).width(w).height(80f); + }).marginLeft(6).width(w).height(80f).update(button -> { + float pw = w; + float pad = 0f; + if(pane.getChildren().first().getPrefHeight() > pane.getHeight()){ + pw = w + 30; + pad = 6; + } + + Cell cell = ((Table)pane.getParent()).getCell(button); + + if(!MathUtils.isEqual(cell.getMinWidth(), pw)){ + cell.width(pw); + cell.padLeft(pad); + pane.getParent().invalidateHierarchy(); + } + }); } void addLocalHosts(Array array){ diff --git a/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java index be1bf4f5b6..1874ec157c 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java @@ -25,8 +25,12 @@ public class TraceDialog extends FloatingDialog { table.row(); table.add(Bundles.format("text.trace.ip", info.ip)); table.row(); + table.add(Bundles.format("text.trace.id", info.uuid)); + table.row(); table.add(Bundles.format("text.trace.modclient", info.modclient)); table.row(); + table.add(Bundles.format("text.trace.android", info.android)); + table.row(); table.add().pad(5); table.row(); diff --git a/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java index 19cb02e7c7..91959c591d 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java @@ -129,7 +129,7 @@ public class PlayerListFragment implements Fragment{ t.addImageButton("icon-ban", 14*2, () -> { ui.showConfirm("$text.confirm", "$text.confirmban", () -> { if(Net.server()) { - netServer.admins.banPlayer(connection.address); + netServer.admins.banPlayerIP(connection.address); Net.kickConnection(player.clientid, KickReason.banned); }else{ NetEvents.handleAdministerRequest(player, AdminAction.ban); diff --git a/core/src/io/anuke/mindustry/world/Placement.java b/core/src/io/anuke/mindustry/world/Placement.java index 278c5240a4..17961930d4 100644 --- a/core/src/io/anuke/mindustry/world/Placement.java +++ b/core/src/io/anuke/mindustry/world/Placement.java @@ -12,7 +12,6 @@ import io.anuke.mindustry.resource.Recipes; import io.anuke.mindustry.world.blocks.Blocks; import io.anuke.mindustry.world.blocks.ProductionBlocks; import io.anuke.ucore.core.Effects; -import io.anuke.ucore.core.Sounds; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.SolidEntity; @@ -41,7 +40,7 @@ public class Placement { state.inventory.addItem(tile.block().drops.item, tile.block().drops.amount); } - if(sound) threads.run(() -> Sounds.play("break")); + if(sound) threads.run(() -> Effects.sound("break", x * tilesize, y * tilesize)); if(!tile.block().isMultiblock() && !tile.isLinked()){ tile.setBlock(Blocks.air); @@ -86,7 +85,7 @@ public class Placement { } }else if(effects) Effects.effect(Fx.place, x * tilesize, y * tilesize); - if(effects && sound) threads.run(() -> Sounds.play("place")); + if(effects && sound) threads.run(() -> Effects.sound("place", x * tilesize, y * tilesize)); } public static boolean validPlace(int x, int y, Block type){ diff --git a/core/src/io/anuke/mindustry/world/WorldGenerator.java b/core/src/io/anuke/mindustry/world/WorldGenerator.java index a38a80a164..96ad8acc58 100644 --- a/core/src/io/anuke/mindustry/world/WorldGenerator.java +++ b/core/src/io/anuke/mindustry/world/WorldGenerator.java @@ -3,7 +3,6 @@ package io.anuke.mindustry.world; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.utils.Array; -import com.badlogic.gdx.utils.IntMap; import com.badlogic.gdx.utils.ObjectMap; import io.anuke.mindustry.Vars; import io.anuke.mindustry.entities.enemies.Enemy; @@ -95,15 +94,4 @@ public class WorldGenerator { return core; } - - private static IntMap map(Object...objects){ - - IntMap out = new IntMap<>(); - - for(int i = 0; i < objects.length; i += 2){ - out.put(Hue.rgb((Color)objects[i]), (Block)objects[i+1]); - } - - return out; - } } diff --git a/core/src/io/anuke/mindustry/world/blocks/types/defense/Turret.java b/core/src/io/anuke/mindustry/world/blocks/types/defense/Turret.java index 27c5ccce5a..0229810710 100644 --- a/core/src/io/anuke/mindustry/world/blocks/types/defense/Turret.java +++ b/core/src/io/anuke/mindustry/world/blocks/types/defense/Turret.java @@ -147,8 +147,8 @@ public class Turret extends Block{ if(Float.isNaN(entity.rotation)){ entity.rotation = 0; } - entity.rotation = Mathf.slerp(entity.rotation, targetRot, - rotatespeed*Timers.delta()); + entity.rotation = Mathf.slerpDelta(entity.rotation, targetRot, + rotatespeed); if(Angles.angleDist(entity.rotation, targetRot) < shootCone && entity.timer.get(timerReload, reload)){ if(shootsound != null && entity.timer.get(timerSound, soundReload)) Effects.sound(shootsound, entity); diff --git a/core/src/io/anuke/mindustry/world/blocks/types/distribution/Teleporter.java b/core/src/io/anuke/mindustry/world/blocks/types/distribution/Teleporter.java index 4934be7441..a24d115058 100644 --- a/core/src/io/anuke/mindustry/world/blocks/types/distribution/Teleporter.java +++ b/core/src/io/anuke/mindustry/world/blocks/types/distribution/Teleporter.java @@ -19,6 +19,7 @@ import io.anuke.ucore.util.Strings; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.Arrays; import static io.anuke.mindustry.Vars.syncBlockState; @@ -55,6 +56,7 @@ public class Teleporter extends PowerBlock{ TeleporterEntity entity = tile.entity(); if(entity != null){ entity.color = data; + Arrays.fill(entity.items, 0); } } diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java b/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java index 9b6132e8ce..92065eb60c 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java @@ -12,10 +12,12 @@ import io.anuke.ucore.UCore; import io.anuke.ucore.util.Strings; import javax.swing.*; +import java.net.NetworkInterface; import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Enumeration; import java.util.Locale; import static io.anuke.mindustry.Vars.*; @@ -98,4 +100,21 @@ public class DesktopPlatform extends Platform { public ThreadProvider getThreadProvider() { return new DefaultThreadImpl(); } + + @Override + public byte[] getUUID() { + try { + Enumeration e = NetworkInterface.getNetworkInterfaces(); + NetworkInterface out; + for(out = e.nextElement(); out.getHardwareAddress() == null && e.hasMoreElements(); out = e.nextElement()); + + byte[] bytes = out.getHardwareAddress(); + byte[] result = new byte[8]; + System.arraycopy(bytes, 0, result, 0, bytes.length); + return result; + }catch (Exception e){ + e.printStackTrace(); + return null; + } + } } diff --git a/html/src/io/anuke/mindustry/client/HtmlLauncher.java b/html/src/io/anuke/mindustry/client/HtmlLauncher.java index e504d0c2db..542f3362d6 100644 --- a/html/src/io/anuke/mindustry/client/HtmlLauncher.java +++ b/html/src/io/anuke/mindustry/client/HtmlLauncher.java @@ -5,6 +5,7 @@ import com.badlogic.gdx.backends.gwt.GwtApplication; import com.badlogic.gdx.backends.gwt.GwtApplicationConfiguration; import com.badlogic.gdx.backends.gwt.preloader.Preloader.PreloaderCallback; import com.badlogic.gdx.backends.gwt.preloader.Preloader.PreloaderState; +import com.badlogic.gdx.utils.Base64Coder; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; @@ -17,8 +18,10 @@ import com.google.gwt.user.client.ui.*; import io.anuke.mindustry.Mindustry; import io.anuke.mindustry.io.Platform; import io.anuke.mindustry.net.Net; +import io.anuke.ucore.core.Settings; import java.util.Date; +import java.util.Random; public class HtmlLauncher extends GwtApplication { static final int WIDTH = 800; @@ -112,6 +115,22 @@ public class HtmlLauncher extends GwtApplication { String ref = Document.get().getReferrer(); return !ref.startsWith("https") && !ref.contains("itch.io"); } + + @Override + public byte[] getUUID(){ + Settings.defaults("uuid", ""); + + String uuid = Settings.getString("uuid"); + if(uuid.isEmpty()){ + byte[] result = new byte[8]; + new Random().nextBytes(result); + uuid = new String(Base64Coder.encode(result)); + Settings.putString("uuid", uuid); + Settings.save(); + return result; + } + return Base64Coder.decode(uuid); + } }; return new Mindustry(); diff --git a/html/src/io/anuke/mindustry/client/WebsocketClient.java b/html/src/io/anuke/mindustry/client/WebsocketClient.java index 66b82fe32a..75bb3339f7 100644 --- a/html/src/io/anuke/mindustry/client/WebsocketClient.java +++ b/html/src/io/anuke/mindustry/client/WebsocketClient.java @@ -7,22 +7,18 @@ import com.badlogic.gdx.utils.reflect.ReflectionException; import com.sksamuel.gwt.websockets.Websocket; import com.sksamuel.gwt.websockets.WebsocketListener; import io.anuke.mindustry.io.Platform; -import io.anuke.mindustry.net.Host; -import io.anuke.mindustry.net.Net; +import io.anuke.mindustry.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; -import static io.anuke.mindustry.Vars.*; +import static io.anuke.mindustry.Vars.webPort; public class WebsocketClient implements ClientProvider { Websocket socket; @@ -104,39 +100,45 @@ public class WebsocketClient implements ClientProvider { @Override public void pingHost(String address, int port, Consumer valid, Consumer failed) { - if(!Platform.instance.canJoinGame()) { - failed.accept(new IOException()); - }else { - Websocket socket = new Websocket("ws://" + address + ":" + 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.")); - } + try { + if (!Platform.instance.canJoinGame()) { + failed.accept(new IOException()); + } else { + Websocket socket = new Websocket("ws://" + address + ":" + 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) { - if(!msg.startsWith("---")) return; - String[] text = msg.substring(3).split("\\|"); - Host host = new Host(text[1], address, text[2], Strings.parseInt(text[3]), Strings.parseInt(text[0])); - valid.accept(host); - accepted[0] = true; - socket.close(); - } + @Override + public void onMessage(String msg) { + byte[] bytes = Base64Coder.decode(msg); + Host host = NetworkIO.readServerData(address, ByteBuffer.wrap(bytes)); + if(bytes.length != 128) + valid.accept(new Host("Unknown", address, "Unknown", 0, 0, 0)); + else + 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 onOpen() { + socket.send("ping"); + } + }); + socket.open(); + Timers.runTask(60f * 5, () -> { + if (!accepted[0]) { + failed.accept(new IOException("Failed to connect to host.")); + socket.close(); + } + }); + } + }catch (Exception e){ + failed.accept(new IOException("Failed to connect to host.")); } } diff --git a/kryonet/src/io/anuke/kryonet/KryoClient.java b/kryonet/src/io/anuke/kryonet/KryoClient.java index 77cf87be44..d9f25b830e 100644 --- a/kryonet/src/io/anuke/kryonet/KryoClient.java +++ b/kryonet/src/io/anuke/kryonet/KryoClient.java @@ -10,6 +10,7 @@ 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.NetworkIO; import io.anuke.mindustry.net.Packets.Connect; import io.anuke.mindustry.net.Packets.Disconnect; import io.anuke.ucore.function.Consumer; @@ -41,7 +42,7 @@ public class KryoClient implements ClientProvider{ @Override public void onDiscoveredHost(DatagramPacket datagramPacket) { ByteBuffer buffer = ByteBuffer.wrap(datagramPacket.getData()); - Host address = KryoRegistrator.readServerData(datagramPacket.getAddress(), buffer); + Host address = NetworkIO.readServerData(datagramPacket.getAddress().getHostAddress(), buffer); addresses.put(datagramPacket.getAddress(), address); } @@ -156,7 +157,7 @@ public class KryoClient implements ClientProvider{ socket.receive(packet); ByteBuffer buffer = ByteBuffer.wrap(packet.getData()); - Host host = KryoRegistrator.readServerData(packet.getAddress(), buffer); + Host host = NetworkIO.readServerData(packet.getAddress().getHostAddress(), buffer); if (host != null) { Gdx.app.postRunnable(() -> valid.accept(host)); diff --git a/kryonet/src/io/anuke/kryonet/KryoRegistrator.java b/kryonet/src/io/anuke/kryonet/KryoRegistrator.java index 007580013b..5d9590dd27 100644 --- a/kryonet/src/io/anuke/kryonet/KryoRegistrator.java +++ b/kryonet/src/io/anuke/kryonet/KryoRegistrator.java @@ -2,15 +2,12 @@ package io.anuke.kryonet; import com.esotericsoftware.minlog.Log; import com.esotericsoftware.minlog.Log.Logger; -import io.anuke.mindustry.net.Host; import io.anuke.ucore.util.ColorCodes; import java.io.PrintWriter; import java.io.StringWriter; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import static io.anuke.mindustry.Vars.*; +import static io.anuke.mindustry.Vars.headless; public class KryoRegistrator { public static boolean fakeLag = false; @@ -45,46 +42,4 @@ public class KryoRegistrator { } }); } - - public static ByteBuffer writeServerData(){ - int maxlen = 32; - - String host = (headless ? "Server" : player.name); - String map = world.getMap().name; - - host = host.substring(0, Math.min(host.length(), maxlen)); - map = map.substring(0, Math.min(map.length(), maxlen)); - - ByteBuffer buffer = ByteBuffer.allocate(128); - - buffer.put((byte)host.getBytes().length); - buffer.put(host.getBytes()); - - buffer.put((byte)map.getBytes().length); - buffer.put(map.getBytes()); - - buffer.putInt(playerGroup.size()); - buffer.putInt(state.wave); - return buffer; - } - - public static Host readServerData(InetAddress ia, ByteBuffer buffer){ - if(buffer.capacity() < 128) return null; //old version address. - - byte hlength = buffer.get(); - byte[] hb = new byte[hlength]; - buffer.get(hb); - - byte mlength = buffer.get(); - byte[] mb = new byte[mlength]; - buffer.get(mb); - - String host = new String(hb); - String map = new String(mb); - - int players = buffer.getInt(); - int wave = buffer.getInt(); - - return new Host(host, ia.getHostAddress(), map, wave, players); - } } diff --git a/kryonet/src/io/anuke/kryonet/KryoServer.java b/kryonet/src/io/anuke/kryonet/KryoServer.java index 24b53ad0da..9e1cd8f27f 100644 --- a/kryonet/src/io/anuke/kryonet/KryoServer.java +++ b/kryonet/src/io/anuke/kryonet/KryoServer.java @@ -10,13 +10,10 @@ 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.*; 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.*; -import io.anuke.mindustry.net.Registrator; -import io.anuke.mindustry.net.Streamable; import io.anuke.mindustry.net.Streamable.StreamBegin; import io.anuke.mindustry.net.Streamable.StreamChunk; import io.anuke.ucore.UCore; @@ -37,11 +34,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import static io.anuke.mindustry.Vars.headless; -import static io.anuke.mindustry.Vars.state; -import static io.anuke.mindustry.Vars.world; public class KryoServer implements ServerProvider { - final boolean debug = false; final Server server; final ByteSerializer serializer = new ByteSerializer(); final ByteBuffer buffer = ByteBuffer.allocate(4096); @@ -56,7 +50,7 @@ public class KryoServer implements ServerProvider { public KryoServer(){ server = new Server(4096*2, 2048, connection -> new ByteSerializer()); server.setDiscoveryHandler((datagramChannel, fromAddress) -> { - ByteBuffer buffer = KryoRegistrator.writeServerData(); + ByteBuffer buffer = NetworkIO.writeServerData(); buffer.position(0); datagramChannel.send(buffer, fromAddress); return true; @@ -183,26 +177,28 @@ public class KryoServer implements ServerProvider { connections.clear(); lastconnection = 0; - Thread thread = new Thread(() -> { - try { - server.close(); - try { - if (webServer != null) webServer.stop(1); - }catch(Exception e){ - e.printStackTrace(); - } - //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);}); + async(server::close); + + //kill them all + for (Thread worker : Thread.getAllStackTraces().keySet()) { + if (worker.getName().contains("WebSocketWorker")) { + worker.interrupt(); } - }); - thread.setDaemon(true); - thread.start(); + } + + try { + if (webServer != null) webServer.stop(1); + }catch (NullPointerException e){ + try { + synchronized (webServer) { + ((Thread) UCore.getPrivate(WebSocketServer.class, webServer, "selectorthread")).join(1); + } + }catch (InterruptedException j){ + handleException(j); + } + }catch (InterruptedException e){ + handleException(e); + } } @Override @@ -290,25 +286,7 @@ public class KryoServer implements ServerProvider { @Override public void dispose(){ - try { - if(serverThread != null) serverThread.interrupt(); - server.dispose(); - }catch (Exception e){ - Log.err(e); - } - - try { - - 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){ - Log.err(e); - } + close(); Log.info("Disposed server."); } @@ -338,25 +316,10 @@ public class KryoServer implements ServerProvider { return null; } - void throwErrorAndExit(){ - Log.err("KRYONET CONNECTIONS:"); - for(Connection c : server.getConnections()){ - NetConnection k = getByKryoID(c.getID()); - Log.err(" - Kryonet connection / ID {0} / IP {1} / NetConnection ID {2}", - c.getID(), c.getRemoteAddressTCP().getAddress().getHostAddress(), k == null ? "NOT FOUND" : k.id); - } - Log.err("NET CONNECTIONS:"); - for(NetConnection c : connections){ - Log.err(" - NetConnection / ID {0} / IP {1}", c.id, c.address); - } - - Log.err("\nSTACK TRACE:"); - - StackTraceElement[] e = Thread.getAllStackTraces().get(Thread.currentThread()); - for(StackTraceElement s : e){ - Log.err("- {0}", s); - } - System.exit(-1); + void async(Runnable run){ + Thread thread = new Thread(run); + thread.setDaemon(true); + thread.start(); } class KryoConnection extends NetConnection{ @@ -381,14 +344,12 @@ public class KryoServer implements ServerProvider { try { synchronized (buffer) { buffer.position(0); - if(debug) Log.info("Sending object with ID {0}", 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) Log.info("Sending string: {0}", string); socket.send(string); } }catch (WebsocketNotConnectedException e){ @@ -443,16 +404,7 @@ public class KryoServer implements ServerProvider { } @Override - public void onOpen(WebSocket conn, ClientHandshake handshake) { - Connect connect = new Connect(); - connect.addressTCP = conn.getRemoteSocketAddress().getAddress().getHostAddress(); - KryoConnection kn = new KryoConnection(lastconnection ++, connect.addressTCP, conn); - - Log.info("&bRecieved web connection: {0} {1}", kn.id, connect.addressTCP); - connections.add(kn); - - Gdx.app.postRunnable(() -> Net.handleServerReceived(kn.id, connect)); - } + public void onOpen(WebSocket conn, ClientHandshake handshake) {} @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { @@ -470,18 +422,31 @@ public class KryoServer implements ServerProvider { @Override public void onMessage(WebSocket conn, String message) { try { - KryoConnection k = getBySocket(conn); - if (k == null) return; - - if(message.equals("_ping_")){ - conn.send("---" + Vars.playerGroup.size() + "|" + (headless ? "Server" : Vars.player.name) - + "|" + world.getMap().name + "|" + state.wave); - connections.remove(k); + if(message.equals("ping")){ + ByteBuffer ping = NetworkIO.writeServerData(); + conn.send(new String(Base64Coder.encode(ping.array()))); }else { + KryoConnection k = getBySocket(conn); + + if (k == null){ + Connect connect = new Connect(); + connect.addressTCP = conn.getRemoteSocketAddress().getAddress().getHostAddress(); + k = new KryoConnection(lastconnection ++, connect.addressTCP, conn); + + Log.info("&bRecieved web connection: {0} {1}", k.id, connect.addressTCP); + connections.add(k); + + int id = k.id; + + Gdx.app.postRunnable(() -> Net.handleServerReceived(id, connect)); + } + + int id = k.id; + byte[] out = Base64Coder.decode(message); ByteBuffer buffer = ByteBuffer.wrap(out); Object o = serializer.read(buffer); - Gdx.app.postRunnable(() -> Net.handleServerReceived(k.id, o)); + Gdx.app.postRunnable(() -> Net.handleServerReceived(id, o)); } }catch (Exception e){ Log.err(e); diff --git a/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index 35f83ef645..0b8da6e4f6 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -9,6 +9,7 @@ import io.anuke.mindustry.game.Difficulty; import io.anuke.mindustry.game.EventType.GameOverEvent; import io.anuke.mindustry.game.GameMode; import io.anuke.mindustry.io.SaveIO; +import io.anuke.mindustry.io.Version; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.NetEvents; import io.anuke.mindustry.net.Packets.ChatPacket; @@ -74,27 +75,24 @@ public class ServerControl extends Module { Events.on(GameOverEvent.class, () -> { info("Game over!"); - Timers.runTask(30f, () -> { + Timers.runTask(10f, () -> { state.set(State.menu); Net.closeServer(); - Timers.runTask(30f, () -> { + if (mode != ShuffleMode.off) { + Array maps = mode == ShuffleMode.both ? world.maps().getAllMaps() : + mode == ShuffleMode.normal ? world.maps().getDefaultMaps() : world.maps().getCustomMaps(); - if (mode != ShuffleMode.off) { - Array maps = mode == ShuffleMode.both ? world.maps().getAllMaps() : - mode == ShuffleMode.normal ? world.maps().getDefaultMaps() : world.maps().getCustomMaps(); + Map previous = world.getMap(); + Map map = previous; + while (map == previous || !map.visible) map = maps.random(); - Map previous = world.getMap(); - Map map = previous; - while (map == previous || !map.visible) map = maps.random(); - - info("Selected next map to be {0}.", map.name); - state.set(State.playing); - logic.reset(); - world.loadMap(map); - host(); - } - }); + info("Selected next map to be {0}.", map.name); + state.set(State.playing); + logic.reset(); + world.loadMap(map); + host(); + } }); }); @@ -109,6 +107,10 @@ public class ServerControl extends Module { } }); + handler.register("version", "Displays server version info.", arg -> { + info("&lmVersion: &lyMindustry {0} {1} / {2}", Version.code, Version.type, Version.buildName); + }); + handler.register("exit", "Exit the server application.", arg -> { info("Shutting down server."); Net.dispose(); @@ -252,7 +254,7 @@ public class ServerControl extends Module { } }); - handler.register("kick", "", "Kick a person by name.", arg -> { + handler.register("kick", "", "Kick a person by name.", arg -> { if(!state.is(State.playing)) { err("Not hosting a game yet. Calm down."); return; @@ -275,7 +277,7 @@ public class ServerControl extends Module { } }); - handler.register("ban", "", "Ban a person by name.", arg -> { + handler.register("ban", "", "Ban a person by name.", arg -> { if(!state.is(State.playing)) { err("Can't ban people by name with no players."); return; @@ -292,29 +294,42 @@ public class ServerControl extends Module { if(target != null){ String ip = Net.getConnection(target.clientid).address; - netServer.admins.banPlayer(ip); + netServer.admins.banPlayerIP(ip); + netServer.admins.banPlayerID(netServer.admins.getTrace(ip).uuid); Net.kickConnection(target.clientid, KickReason.banned); - info("Banned player by IP: {0}", ip); + info("Banned player by IP and ID: {0} / {1}", ip, netServer.admins.getTrace(ip).uuid); }else{ info("Nobody with that name could be found."); } }); - handler.register("bans", "List all banned IPs.", arg -> { + handler.register("bans", "List all banned IPs and IDs.", arg -> { Array bans = netServer.admins.getBanned(); if(bans.size == 0){ - Log.info("No banned players have been found."); + Log.info("No IP-banned players have been found."); }else{ - Log.info("&lyBanned players:"); + Log.info("&lyBanned players [IP]:"); for(String string : bans){ - Log.info(" &luy {0} / Last known name: '{1}'", string, netServer.admins.getLastName(string)); + Log.info(" &ly {0} / Last known name: '{1}'", string, netServer.admins.getLastName(string)); + } + } + + Array idbans = netServer.admins.getBannedIDs(); + + if(idbans.size == 0){ + Log.info("No ID-banned players have been found."); + }else{ + Log.info("&lmBanned players [ID]:"); + for(String string : idbans){ + Log.info(" &lm '{0}' / Last known name: '{1}' / Last known IP: '{2}'", string, + netServer.admins.getLastName(netServer.admins.getLastIP(string)), netServer.admins.getLastIP(string)); } } }); handler.register("banip", "", "Ban a person by IP.", arg -> { - if(netServer.admins.banPlayer(arg[0])) { + if(netServer.admins.banPlayerIP(arg[0])) { info("Banned player by IP: {0}.", arg[0]); for(Player player : playerGroup.all()){ @@ -328,15 +343,49 @@ public class ServerControl extends Module { } }); - handler.register("unbanip", "", "Unban a person by IP.", arg -> { - if(netServer.admins.unbanPlayer(arg[0])) { + handler.register("banid", "", "Ban a person by their unique ID.", arg -> { + if(netServer.admins.banPlayerID(arg[0])) { + info("Banned player by ID: {0}.", arg[0]); + + for(Player player : playerGroup.all()){ + if(netServer.admins.getTrace(Net.getConnection(player.clientid).address).uuid.equals(arg[0])){ + Net.kickConnection(player.clientid, KickReason.banned); + break; + } + } + }else{ + err("That ID is already banned!"); + } + }); + + handler.register("unbanip", "", "Completely unban a person by IP.", arg -> { + if(netServer.admins.unbanPlayerIP(arg[0])) { info("Unbanned player by IP: {0}.", arg[0]); + for(String s : netServer.admins.getBannedIDs()){ + if(netServer.admins.getLastIP(s).equals(arg[0])){ + netServer.admins.unbanPlayerID(s); + Log.info("Also unbanned UUID '{0}' as it corresponds to this IP.", s); + } + } }else{ err("That IP is not banned!"); } }); - handler.register("admin", "", "Make a user admin", arg -> { + handler.register("unbanid", "", "Completely unban a person by ID.", arg -> { + if(netServer.admins.unbanPlayerID(arg[0])) { + info("&lmUnbanned player by ID: {0}.", arg[0]); + String ip = netServer.admins.getLastIP(arg[0]); + if(!ip.equals("unknown")) { + netServer.admins.unbanPlayerIP(ip); + Log.info("Also unbanned IP '{0}' as it corresponds to this ID.", ip); + } + }else{ + err("That IP is not banned!"); + } + }); + + handler.register("admin", "", "Make a user admin", arg -> { if(!state.is(State.playing)) { err("Open the server first."); return; @@ -361,7 +410,7 @@ public class ServerControl extends Module { } }); - handler.register("unadmin", "", "Removes admin status from a player", arg -> { + handler.register("unadmin", "", "Removes admin status from a player", arg -> { if(!state.is(State.playing)) { err("Open the server first."); return; @@ -449,6 +498,11 @@ public class ServerControl extends Module { }); handler.register("gameover", "Force a game over.", arg -> { + if(state.is(State.menu)){ + info("Not playing a map."); + return; + } + world.removeBlock(world.getCore()); info("Core destroyed."); }); @@ -484,7 +538,7 @@ public class ServerControl extends Module { } }); - handler.register("trace", "", "Trace a player's actions", arg -> { + handler.register("trace", "", "Trace a player's actions", arg -> { if(!state.is(State.playing)) { err("Open the server first."); return; @@ -504,7 +558,9 @@ public class ServerControl extends Module { Log.info("&lcTrace info for player '{0}':", target.name); Log.info(" &lyEntity ID: {0}", info. playerid); Log.info(" &lyIP: {0}", info.ip); + Log.info(" &lyUUID: {0}", info.uuid); Log.info(" &lycustom client: {0}", info.modclient); + Log.info(" &lyandroid: {0}", info.android); Log.info(""); Log.info(" &lytotal blocks broken: {0}", info.totalBlocksBroken); Log.info(" &lystructure blocks broken: {0}", info.structureBlocksBroken);