diff --git a/android/src/io/anuke/mindustry/AndroidLauncher.java b/android/src/io/anuke/mindustry/AndroidLauncher.java index 8870d5b567..ea6a1443a5 100644 --- a/android/src/io/anuke/mindustry/AndroidLauncher.java +++ b/android/src/io/anuke/mindustry/AndroidLauncher.java @@ -6,6 +6,7 @@ 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; @@ -97,6 +98,24 @@ public class AndroidLauncher extends AndroidApplication{ public boolean isDebug() { return false; } + + @Override + public byte[] getUUID() { + try { + String s = Secure.getString(getContext().getContentResolver(), + Secure.ANDROID_ID); + 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/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 31897ebbbf..eff18ffebc 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 diff --git a/core/assets/version.properties b/core/assets/version.properties index d62f17fc2b..bac2905c1f 100644 --- a/core/assets/version.properties +++ b/core/assets/version.properties @@ -1,7 +1,7 @@ #Autogenerated file. Do not modify. -#Sat Mar 03 12:04:38 EST 2018 +#Tue Mar 06 19:13:04 EST 2018 version=release -androidBuildCode=333 +androidBuildCode=335 name=Mindustry code=3.4 -build=30 +build=31 diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index aa8eb26f79..a3c68f1f00 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; @@ -68,11 +67,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..bca30b9f46 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -1,9 +1,6 @@ package io.anuke.mindustry.core; -import com.badlogic.gdx.utils.ByteArray; -import com.badlogic.gdx.utils.IntMap; -import com.badlogic.gdx.utils.ObjectMap; -import com.badlogic.gdx.utils.TimeUtils; +import com.badlogic.gdx.utils.*; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.entities.SyncEntity; @@ -51,18 +48,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 +276,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/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/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index a69072a6dc..c639005cfb 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); } } @@ -625,6 +630,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); @@ -632,6 +638,7 @@ public class Packets { buffer.putInt(info.totalBlocksPlaced); buffer.putInt(info.lastBlockPlaced.id); + buffer.put(Base64Coder.decode(info.uuid)); } @Override @@ -645,11 +652,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/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/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/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java b/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java index 9b6132e8ce..5540538025 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java @@ -12,6 +12,7 @@ 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; @@ -98,4 +99,17 @@ public class DesktopPlatform extends Platform { public ThreadProvider getThreadProvider() { return new DefaultThreadImpl(); } + + @Override + public byte[] getUUID() { + try { + byte[] bytes = NetworkInterface.getNetworkInterfaces().nextElement().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/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index 9ffe8b585a..a125947f5a 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -252,7 +252,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 +275,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 +292,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(" &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 +341,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 +408,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; @@ -484,7 +531,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 +551,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);