it is done

This commit is contained in:
Anuken
2019-12-25 01:39:38 -05:00
parent 5b21873f3c
commit 514d4817c8
488 changed files with 4572 additions and 4574 deletions

View File

@@ -0,0 +1,349 @@
package mindustry.net;
import arc.*;
import mindustry.annotations.Annotations.*;
import arc.struct.*;
import mindustry.Vars;
import static mindustry.Vars.headless;
import static mindustry.game.EventType.*;
public class Administration{
/** All player info. Maps UUIDs to info. This persists throughout restarts. */
private ObjectMap<String, PlayerInfo> playerInfo = new ObjectMap<>();
private Array<String> bannedIPs = new Array<>();
private Array<String> whitelist = new Array<>();
public Administration(){
Core.settings.defaults(
"strict", true,
"servername", "Server"
);
load();
}
public int getPlayerLimit(){
return Core.settings.getInt("playerlimit", 0);
}
public void setPlayerLimit(int limit){
Core.settings.putSave("playerlimit", limit);
}
public void setStrict(boolean on){
Core.settings.putSave("strict", on);
}
public boolean getStrict(){
return Core.settings.getBool("strict");
}
public boolean allowsCustomClients(){
return Core.settings.getBool("allow-custom", !headless);
}
public void setCustomClients(boolean allowed){
Core.settings.put("allow-custom", allowed);
Core.settings.save();
}
/** Call when a player joins to update their information here. */
public void updatePlayerJoined(String id, String ip, String name){
PlayerInfo info = getCreateInfo(id);
info.lastName = name;
info.lastIP = ip;
info.timesJoined++;
if(!info.names.contains(name, false)) info.names.add(name);
if(!info.ips.contains(ip, false)) info.ips.add(ip);
}
public boolean banPlayer(String uuid){
return banPlayerID(uuid) || banPlayerIP(getInfo(uuid).lastIP);
}
/**
* Bans a player by IP; returns whether this player was already banned.
* If there are players who at any point had this IP, they will be UUID banned as well.
*/
public boolean banPlayerIP(String ip){
if(bannedIPs.contains(ip, false))
return false;
for(PlayerInfo info : playerInfo.values()){
if(info.ips.contains(ip, false)){
info.banned = true;
}
}
bannedIPs.add(ip);
save();
Events.fire(new PlayerIpBanEvent(ip));
return true;
}
/** Bans a player by UUID; returns whether this player was already banned. */
public boolean banPlayerID(String id){
if(playerInfo.containsKey(id) && playerInfo.get(id).banned)
return false;
getCreateInfo(id).banned = true;
save();
Events.fire(new PlayerBanEvent(Vars.playerGroup.find(p -> id.equals(p.uuid))));
return true;
}
/**
* Unbans a player by IP; returns whether this player was banned in the first place.
* This method also unbans any player that was banned and had this IP.
*/
public boolean unbanPlayerIP(String ip){
boolean found = bannedIPs.contains(ip, false);
for(PlayerInfo info : playerInfo.values()){
if(info.ips.contains(ip, false)){
info.banned = false;
found = true;
}
}
bannedIPs.removeValue(ip, false);
if(found){
save();
Events.fire(new PlayerIpUnbanEvent(ip));
}
return found;
}
/**
* Unbans a player by ID; returns whether this player was banned in the first place.
* This also unbans all IPs the player used.
*/
public boolean unbanPlayerID(String id){
PlayerInfo info = getCreateInfo(id);
if(!info.banned)
return false;
info.banned = false;
bannedIPs.removeAll(info.ips, false);
save();
Events.fire(new PlayerUnbanEvent(Vars.playerGroup.find(p -> id.equals(p.uuid))));
return true;
}
/**
* Returns list of all players with admin status
*/
public Array<PlayerInfo> getAdmins(){
Array<PlayerInfo> result = new Array<>();
for(PlayerInfo info : playerInfo.values()){
if(info.admin){
result.add(info);
}
}
return result;
}
/**
* Returns list of all players with admin status
*/
public Array<PlayerInfo> getBanned(){
Array<PlayerInfo> result = new Array<>();
for(PlayerInfo info : playerInfo.values()){
if(info.banned){
result.add(info);
}
}
return result;
}
/**
* Returns all banned IPs. This does not include the IPs of ID-banned players.
*/
public Array<String> getBannedIPs(){
return bannedIPs;
}
/**
* Makes a player an admin. Returns whether this player was already an admin.
*/
public boolean adminPlayer(String id, String usid){
PlayerInfo info = getCreateInfo(id);
if(info.admin && info.adminUsid != null && info.adminUsid.equals(usid))
return false;
info.adminUsid = usid;
info.admin = true;
save();
return true;
}
/**
* Makes a player no longer an admin. Returns whether this player was an admin in the first place.
*/
public boolean unAdminPlayer(String id){
PlayerInfo info = getCreateInfo(id);
if(!info.admin)
return false;
info.admin = false;
save();
return true;
}
public boolean isWhitelistEnabled(){
return Core.settings.getBool("whitelist", false);
}
public void setWhitelist(boolean enabled){
Core.settings.putSave("whitelist", enabled);
}
public boolean isWhitelisted(String id, String usid){
return !isWhitelistEnabled() || whitelist.contains(usid + id);
}
public boolean whitelist(String id){
PlayerInfo info = getCreateInfo(id);
if(whitelist.contains(info.adminUsid + id)) return false;
whitelist.add(info.adminUsid + id);
save();
return true;
}
public boolean unwhitelist(String id){
PlayerInfo info = getCreateInfo(id);
if(whitelist.contains(info.adminUsid + id)){
whitelist.remove(info.adminUsid + id);
save();
return true;
}
return false;
}
public boolean isIPBanned(String ip){
return bannedIPs.contains(ip, false) || (findByIP(ip) != null && findByIP(ip).banned);
}
public boolean isIDBanned(String uuid){
return getCreateInfo(uuid).banned;
}
public boolean isAdmin(String id, String usid){
PlayerInfo info = getCreateInfo(id);
return info.admin && usid.equals(info.adminUsid);
}
/** Finds player info by IP, UUID and name. */
public ObjectSet<PlayerInfo> findByName(String name){
ObjectSet<PlayerInfo> result = new ObjectSet<>();
for(PlayerInfo info : playerInfo.values()){
if(info.lastName.toLowerCase().equals(name.toLowerCase()) || (info.names.contains(name, false))
|| info.ips.contains(name, false) || info.id.equals(name)){
result.add(info);
}
}
return result;
}
public Array<PlayerInfo> findByIPs(String ip){
Array<PlayerInfo> result = new Array<>();
for(PlayerInfo info : playerInfo.values()){
if(info.ips.contains(ip, false)){
result.add(info);
}
}
return result;
}
public PlayerInfo getInfo(String id){
return getCreateInfo(id);
}
public PlayerInfo getInfoOptional(String id){
return playerInfo.get(id);
}
public PlayerInfo findByIP(String ip){
for(PlayerInfo info : playerInfo.values()){
if(info.ips.contains(ip, false)){
return info;
}
}
return null;
}
public Array<PlayerInfo> getWhitelisted(){
return playerInfo.values().toArray().select(p -> isWhitelisted(p.id, p.adminUsid));
}
private PlayerInfo getCreateInfo(String id){
if(playerInfo.containsKey(id)){
return playerInfo.get(id);
}else{
PlayerInfo info = new PlayerInfo(id);
playerInfo.put(id, info);
save();
return info;
}
}
public void save(){
Core.settings.putObject("player-info", playerInfo);
Core.settings.putObject("banned-ips", bannedIPs);
Core.settings.putObject("whitelisted", whitelist);
Core.settings.save();
}
@SuppressWarnings("unchecked")
private void load(){
playerInfo = Core.settings.getObject("player-info", ObjectMap.class, ObjectMap::new);
bannedIPs = Core.settings.getObject("banned-ips", Array.class, Array::new);
whitelist = Core.settings.getObject("whitelisted", Array.class, Array::new);
}
@Serialize
public static class PlayerInfo{
public String id;
public String lastName = "<unknown>", lastIP = "<unknown>";
public Array<String> ips = new Array<>();
public Array<String> names = new Array<>();
public String adminUsid;
public int timesKicked;
public int timesJoined;
public boolean banned, admin;
public long lastKicked; //last kicked timestamp
PlayerInfo(String id){
this.id = id;
}
public PlayerInfo(){
}
}
public static class TraceInfo{
public String ip, uuid;
public boolean modded, mobile;
public TraceInfo(String ip, String uuid, boolean modded, boolean mobile){
this.ip = ip;
this.uuid = uuid;
this.modded = modded;
this.mobile = mobile;
}
}
}

View File

@@ -0,0 +1,426 @@
package mindustry.net;
import arc.*;
import arc.struct.*;
import arc.func.*;
import arc.net.*;
import arc.net.FrameworkMessage.*;
import arc.util.*;
import arc.util.async.*;
import arc.util.pooling.*;
import mindustry.net.Net.*;
import mindustry.net.Packets.*;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.concurrent.*;
import static mindustry.Vars.*;
public class ArcNetProvider implements NetProvider{
final Client client;
final Prov<DatagramPacket> packetSupplier = () -> new DatagramPacket(new byte[256], 256);
final Server server;
final CopyOnWriteArrayList<ArcConnection> connections = new CopyOnWriteArrayList<>();
Thread serverThread;
public ArcNetProvider(){
client = new Client(8192, 4096, new PacketSerializer());
client.setDiscoveryPacket(packetSupplier);
client.addListener(new NetListener(){
@Override
public void connected(Connection connection){
Connect c = new Connect();
c.addressTCP = connection.getRemoteAddressTCP().getAddress().getHostAddress();
if(connection.getRemoteAddressTCP() != null) c.addressTCP = connection.getRemoteAddressTCP().toString();
Core.app.post(() -> net.handleClientReceived(c));
}
@Override
public void disconnected(Connection connection, DcReason reason){
if(connection.getLastProtocolError() != null){
netClient.setQuiet();
}
Disconnect c = new Disconnect();
c.reason = reason.toString();
Core.app.post(() -> net.handleClientReceived(c));
}
@Override
public void received(Connection connection, Object object){
if(object instanceof FrameworkMessage) return;
Core.app.post(() -> {
try{
net.handleClientReceived(object);
}catch(Exception e){
handleException(e);
}
});
}
});
server = new Server(4096 * 2, 4096, new PacketSerializer());
server.setMulticast(multicastGroup, multicastPort);
server.setDiscoveryHandler((address, handler) -> {
ByteBuffer buffer = NetworkIO.writeServerData();
buffer.position(0);
handler.respond(buffer);
});
server.addListener(new NetListener(){
@Override
public void connected(Connection connection){
String ip = connection.getRemoteAddressTCP().getAddress().getHostAddress();
ArcConnection kn = new ArcConnection(ip, connection);
Connect c = new Connect();
c.addressTCP = ip;
Log.debug("&bRecieved connection: {0}", c.addressTCP);
connections.add(kn);
Core.app.post(() -> net.handleServerReceived(kn, c));
}
@Override
public void disconnected(Connection connection, DcReason reason){
ArcConnection k = getByArcID(connection.getID());
if(k == null) return;
Disconnect c = new Disconnect();
c.reason = reason.toString();
Core.app.post(() -> {
net.handleServerReceived(k, c);
connections.remove(k);
});
}
@Override
public void received(Connection connection, Object object){
ArcConnection k = getByArcID(connection.getID());
if(object instanceof FrameworkMessage || k == null) return;
Core.app.post(() -> {
try{
net.handleServerReceived(k, object);
}catch(RuntimeException e){
if(e.getCause() instanceof ValidateException){
ValidateException v = (ValidateException)e.getCause();
Log.err("Validation failed: {0} ({1})", v.player.name, v.getMessage());
}else{
e.printStackTrace();
}
}catch(Exception e){
e.printStackTrace();
}
});
}
});
}
private static boolean isLocal(InetAddress addr){
if(addr.isAnyLocalAddress() || addr.isLoopbackAddress()) return true;
try{
return NetworkInterface.getByInetAddress(addr) != null;
}catch(Exception e){
return false;
}
}
@Override
public void connectClient(String ip, int port, Runnable success){
Threads.daemon(() -> {
try{
//just in case
client.stop();
Threads.daemon("Net Client", () -> {
try{
client.run();
}catch(Exception e){
if(!(e instanceof ClosedSelectorException)) handleException(e);
}
});
client.connect(5000, ip, port, port);
success.run();
}catch(Exception e){
handleException(e);
}
});
}
@Override
public void disconnectClient(){
client.close();
}
@Override
public void sendClient(Object object, SendMode mode){
try{
if(mode == SendMode.tcp){
client.sendTCP(object);
}else{
client.sendUDP(object);
}
//sending things can cause an under/overflow, catch it and disconnect instead of crashing
}catch(BufferOverflowException | BufferUnderflowException e){
net.showError(e);
}
Pools.free(object);
}
@Override
public void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> invalid){
Threads.daemon(() -> {
try{
DatagramSocket socket = new DatagramSocket();
socket.send(new DatagramPacket(new byte[]{-2, 1}, 2, InetAddress.getByName(address), port));
socket.setSoTimeout(2000);
DatagramPacket packet = packetSupplier.get();
socket.receive(packet);
ByteBuffer buffer = ByteBuffer.wrap(packet.getData());
Host host = NetworkIO.readServerData(packet.getAddress().getHostAddress(), buffer);
Core.app.post(() -> valid.get(host));
}catch(Exception e){
Core.app.post(() -> invalid.get(e));
}
});
}
@Override
public void discoverServers(Cons<Host> callback, Runnable done){
Array<InetAddress> foundAddresses = new Array<>();
client.discoverHosts(port, multicastGroup, multicastPort, 3000, packet -> {
Core.app.post(() -> {
try{
if(foundAddresses.contains(address -> address.equals(packet.getAddress()) || (isLocal(address) && isLocal(packet.getAddress())))){
return;
}
ByteBuffer buffer = ByteBuffer.wrap(packet.getData());
Host host = NetworkIO.readServerData(packet.getAddress().getHostAddress(), buffer);
callback.get(host);
foundAddresses.add(packet.getAddress());
}catch(Exception e){
//don't crash when there's an error pinging a a server or parsing data
e.printStackTrace();
}
});
}, () -> Core.app.post(done));
}
@Override
public void dispose(){
disconnectClient();
closeServer();
try{
client.dispose();
}catch(IOException ignored){
}
}
@Override
public Iterable<ArcConnection> getConnections(){
return connections;
}
@Override
public void hostServer(int port) throws IOException{
connections.clear();
server.bind(port, port);
serverThread = new Thread(() -> {
try{
server.run();
}catch(Throwable e){
if(!(e instanceof ClosedSelectorException)) Threads.throwAppException(e);
}
}, "Net Server");
serverThread.setDaemon(true);
serverThread.start();
}
@Override
public void closeServer(){
connections.clear();
Threads.daemon(server::stop);
}
ArcConnection getByArcID(int id){
for(int i = 0; i < connections.size(); i++){
ArcConnection con = connections.get(i);
if(con.connection != null && con.connection.getID() == id){
return con;
}
}
return null;
}
private void handleException(Exception e){
if(e instanceof ArcNetException){
Core.app.post(() -> net.showError(new IOException("mismatch")));
}else if(e instanceof ClosedChannelException){
Core.app.post(() -> net.showError(new IOException("alreadyconnected")));
}else{
Core.app.post(() -> net.showError(e));
}
}
class ArcConnection extends NetConnection{
public final Connection connection;
public ArcConnection(String address, Connection connection){
super(address);
this.connection = connection;
}
@Override
public boolean isConnected(){
return connection.isConnected();
}
@Override
public void sendStream(Streamable stream){
connection.addListener(new InputStreamSender(stream.stream, 512){
int id;
@Override
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 = Registrator.getID(stream.getClass());
connection.sendTCP(begin);
id = begin.id;
}
@Override
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.
}
});
}
@Override
public void send(Object object, SendMode mode){
try{
if(mode == SendMode.tcp){
connection.sendTCP(object);
}else{
connection.sendUDP(object);
}
}catch(Exception e){
Log.err(e);
Log.info("Error sending packet. Disconnecting invalid client!");
connection.close(DcReason.error);
ArcConnection k = getByArcID(connection.getID());
if(k != null) connections.remove(k);
}
}
@Override
public void close(){
if(connection.isConnected()) connection.close(DcReason.closed);
}
}
@SuppressWarnings("unchecked")
public static class PacketSerializer implements NetSerializer{
static Cons2<Packet, ByteBuffer> writer = Packet::write;
@Override
public Object read(ByteBuffer byteBuffer){
byte id = byteBuffer.get();
if(id == -2){
return readFramework(byteBuffer);
}else{
Packet packet = Pools.obtain((Class<Packet>)Registrator.getByID(id).type, (Prov<Packet>)Registrator.getByID(id).constructor);
packet.read(byteBuffer);
return packet;
}
}
@Override
public void write(ByteBuffer byteBuffer, Object o){
if(o instanceof FrameworkMessage){
byteBuffer.put((byte)-2); //code for framework message
writeFramework(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: " + o.getClass());
byteBuffer.put(id);
writer.get((Packet)o, byteBuffer);
}
}
public void writeFramework(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){
buffer.put((byte)1);
}else if(message instanceof KeepAlive){
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 FrameworkMessage readFramework(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){
return new DiscoverHost();
}else if(id == 2){
return new KeepAlive();
}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!");
}
}
}
}

View File

@@ -0,0 +1,158 @@
package mindustry.net;
import arc.*;
import arc.Net.*;
import arc.struct.*;
import arc.files.*;
import arc.func.*;
import arc.util.*;
import arc.util.io.*;
import arc.util.serialization.*;
import arc.util.serialization.JsonValue.*;
import arc.util.serialization.JsonWriter.*;
import mindustry.*;
import mindustry.core.*;
import java.io.*;
import java.text.*;
import java.util.*;
import static mindustry.Vars.net;
public class CrashSender{
public static void send(Throwable exception, Cons<File> writeListener){
try{
exception.printStackTrace();
//don't create crash logs for custom builds, as it's expected
if(Version.build == -1 || (System.getProperty("user.name").equals("anuke") && "release".equals(Version.modifier))) return;
//attempt to load version regardless
if(Version.number == 0){
try{
ObjectMap<String, String> map = new ObjectMap<>();
PropertiesUtils.load(map, new InputStreamReader(CrashSender.class.getResourceAsStream("/version.properties")));
Version.type = map.get("type");
Version.number = Integer.parseInt(map.get("number"));
Version.modifier = map.get("modifier");
if(map.get("build").contains(".")){
String[] split = map.get("build").split("\\.");
Version.build = Integer.parseInt(split[0]);
Version.revision = Integer.parseInt(split[1]);
}else{
Version.build = Strings.canParseInt(map.get("build")) ? Integer.parseInt(map.get("build")) : -1;
}
}catch(Throwable e){
e.printStackTrace();
Log.err("Failed to parse version.");
}
}
try{
File file = new File(OS.getAppDataDirectoryString(Vars.appName), "crashes/crash-report-" + new SimpleDateFormat("MM_dd_yyyy_HH_mm_ss").format(new Date()) + ".txt");
new Fi(OS.getAppDataDirectoryString(Vars.appName)).child("crashes").mkdirs();
new Fi(file).writeString(parseException(exception));
writeListener.get(file);
}catch(Throwable e){
Log.err("Failed to save local crash report.", e);
}
try{
//check crash report setting
if(!Core.settings.getBool("crashreport", true)){
return;
}
}catch(Throwable ignored){
//if there's no settings init we don't know what the user wants but chances are it's an important crash, so send it anyway
}
try{
//check any mods - if there are any, don't send reports
if(Vars.mods != null && !Vars.mods.list().isEmpty()){
return;
}
}catch(Throwable ignored){
}
//do not send exceptions that occur for versions that can't be parsed
if(Version.number == 0){
return;
}
boolean netActive = false, netServer = false;
//attempt to close connections, if applicable
try{
netActive = net.active();
netServer = net.server();
net.dispose();
}catch(Throwable ignored){
}
JsonValue value = new JsonValue(ValueType.object);
boolean fn = netActive, fs = netServer;
//add all relevant info, ignoring exceptions
ex(() -> value.addChild("versionType", new JsonValue(Version.type)));
ex(() -> value.addChild("versionNumber", new JsonValue(Version.number)));
ex(() -> value.addChild("versionModifier", new JsonValue(Version.modifier)));
ex(() -> value.addChild("build", new JsonValue(Version.build)));
ex(() -> value.addChild("revision", new JsonValue(Version.revision)));
ex(() -> value.addChild("net", new JsonValue(fn)));
ex(() -> value.addChild("server", new JsonValue(fs)));
ex(() -> value.addChild("players", new JsonValue(Vars.playerGroup.size())));
ex(() -> value.addChild("state", new JsonValue(Vars.state.getState().name())));
ex(() -> value.addChild("os", new JsonValue(System.getProperty("os.name") + "x" + (OS.is64Bit ? "64" : "32"))));
ex(() -> value.addChild("trace", new JsonValue(parseException(exception))));
ex(() -> value.addChild("javaVersion", new JsonValue(System.getProperty("java.version"))));
ex(() -> value.addChild("javaArch", new JsonValue(System.getProperty("sun.arch.data.model"))));
boolean[] sent = {false};
Log.info("Sending crash report.");
//post to crash report URL
httpPost(Vars.crashReportURL, value.toJson(OutputType.json), r -> {
Log.info("Crash sent successfully.");
sent[0] = true;
System.exit(1);
}, t -> {
t.printStackTrace();
sent[0] = true;
System.exit(1);
});
//sleep until report is sent
try{
while(!sent[0]){
Thread.sleep(30);
}
}catch(InterruptedException ignored){
}
}catch(Throwable death){
death.printStackTrace();
System.exit(1);
}
}
private static void httpPost(String url, String content, Cons<HttpResponse> success, Cons<Throwable> failure){
new NetJavaImpl().http(new HttpRequest().method(HttpMethod.POST).content(content).url(url), success, failure);
}
private static String parseException(Throwable e){
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
private static void ex(Runnable r){
try{
r.run();
}catch(Throwable ignored){
}
}
}

View File

@@ -0,0 +1,27 @@
package mindustry.net;
import mindustry.game.*;
public class Host{
public final String name;
public final String address;
public final String mapname;
public final int wave;
public final int players, playerLimit;
public final int version;
public final String versionType;
public final Gamemode mode;
public int ping;
public Host(String name, String address, String mapname, int wave, int players, int version, String versionType, Gamemode mode, int playerLimit){
this.name = name;
this.address = address;
this.players = players;
this.mapname = mapname;
this.wave = wave;
this.version = version;
this.versionType = versionType;
this.playerLimit = playerLimit;
this.mode = mode;
}
}

View File

@@ -0,0 +1,68 @@
package mindustry.net;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
public class Interpolator{
//used for movement
public Vector2 target = new Vector2();
public Vector2 last = new Vector2();
public float[] targets = {};
public float[] lasts = {};
public long lastUpdated, updateSpacing;
//current state
public Vector2 pos = new Vector2();
public float[] values = {};
public void read(float cx, float cy, float x, float y, float... target1ds){
if(lastUpdated != 0) updateSpacing = Time.timeSinceMillis(lastUpdated);
lastUpdated = Time.millis();
targets = target1ds;
if(lasts.length != values.length){
lasts = new float[values.length];
}
for(int i = 0; i < values.length; i++){
lasts[i] = values[i];
}
last.set(cx, cy);
target.set(x, y);
}
public void reset(){
values = new float[0];
targets = new float[0];
target.setZero();
last.setZero();
lastUpdated = 0;
updateSpacing = 16; //1 frame
pos.setZero();
}
public void update(){
if(lastUpdated != 0 && updateSpacing != 0){
float timeSinceUpdate = Time.timeSinceMillis(lastUpdated);
float alpha = Math.min(timeSinceUpdate / updateSpacing, 2f);
pos.set(last).lerpPast(target, alpha);
if(values.length != targets.length){
values = new float[targets.length];
}
if(lasts.length != targets.length){
lasts = new float[targets.length];
}
for(int i = 0; i < values.length; i++){
values[i] = Mathf.slerp(lasts[i], targets[i], alpha);
}
}else{
pos.set(target);
}
}
}

View File

@@ -0,0 +1,347 @@
package mindustry.net;
import arc.*;
import arc.struct.*;
import arc.func.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import arc.util.pooling.*;
import mindustry.gen.*;
import mindustry.net.Packets.*;
import mindustry.net.Streamable.*;
import net.jpountz.lz4.*;
import java.io.*;
import java.nio.*;
import static mindustry.Vars.*;
@SuppressWarnings("unchecked")
public class Net{
private boolean server;
private boolean active;
private boolean clientLoaded;
private @Nullable
StreamBuilder currentStream;
private final Array<Object> packetQueue = new Array<>();
private final ObjectMap<Class<?>, Cons> clientListeners = new ObjectMap<>();
private final ObjectMap<Class<?>, Cons2<NetConnection, Object>> serverListeners = new ObjectMap<>();
private final IntMap<StreamBuilder> streams = new IntMap<>();
private final NetProvider provider;
private final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor();
private final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor();
public Net(NetProvider provider){
this.provider = provider;
}
/** Display a network error. Call on the graphics thread. */
public void showError(Throwable e){
if(!headless){
Throwable t = e;
while(t.getCause() != null){
t = t.getCause();
}
String baseError = Strings.getFinalMesage(e);
String error = baseError == null ? "" : baseError.toLowerCase();
String type = t.getClass().toString().toLowerCase();
boolean isError = false;
if(e instanceof BufferUnderflowException || e instanceof BufferOverflowException){
error = Core.bundle.get("error.io");
}else if(error.equals("mismatch")){
error = Core.bundle.get("error.mismatch");
}else if(error.contains("port out of range") || error.contains("invalid argument") || (error.contains("invalid") && error.contains("address")) || Strings.parseException(e, true).contains("address associated")){
error = Core.bundle.get("error.invalidaddress");
}else if(error.contains("connection refused") || error.contains("route to host") || type.contains("unknownhost")){
error = Core.bundle.get("error.unreachable");
}else if(type.contains("timeout")){
error = Core.bundle.get("error.timedout");
}else if(error.equals("alreadyconnected") || error.contains("connection is closed")){
error = Core.bundle.get("error.alreadyconnected");
}else if(!error.isEmpty()){
error = Core.bundle.get("error.any");
isError = true;
}
if(isError){
ui.showException("$error.any", e);
}else{
ui.showText("", Core.bundle.format("connectfail", error));
}
ui.loadfrag.hide();
if(client()){
netClient.disconnectQuietly();
}
}
Log.err(e);
}
/**
* Sets the client loaded status, or whether it will recieve normal packets from the server.
*/
public void setClientLoaded(boolean loaded){
clientLoaded = loaded;
if(loaded){
//handle all packets that were skipped while loading
for(int i = 0; i < packetQueue.size; i++){
handleClientReceived(packetQueue.get(i));
}
}
//clear inbound packet queue
packetQueue.clear();
}
public void setClientConnected(){
active = true;
server = false;
}
/**
* Connect to an address.
*/
public void connect(String ip, int port, Runnable success){
try{
if(!active){
provider.connectClient(ip, port, success);
active = true;
server = false;
}else{
throw new IOException("alreadyconnected");
}
}catch(IOException e){
showError(e);
}
}
/**
* Host a server at an address.
*/
public void host(int port) throws IOException{
provider.hostServer(port);
active = true;
server = true;
Time.runTask(60f, platform::updateRPC);
}
/**
* Closes the server.
*/
public void closeServer(){
for(NetConnection con : getConnections()){
Call.onKick(con, KickReason.serverClose);
}
provider.closeServer();
server = false;
active = false;
}
public void reset(){
closeServer();
netClient.disconnectNoReset();
}
public void disconnect(){
provider.disconnectClient();
server = false;
active = false;
}
public byte[] compressSnapshot(byte[] input){
return compressor.compress(input);
}
public byte[] decompressSnapshot(byte[] input, int size){
return decompressor.decompress(input, size);
}
/**
* Starts discovering servers on a different thread.
* Callback is run on the main libGDX thread.
*/
public void discoverServers(Cons<Host> cons, Runnable done){
provider.discoverServers(cons, done);
}
/**
* Returns a list of all connections IDs.
*/
public Iterable<NetConnection> getConnections(){
return (Iterable<NetConnection>)provider.getConnections();
}
/** Send an object to all connected clients, or to the server if this is a client.*/
public void send(Object object, SendMode mode){
if(server){
for(NetConnection con : provider.getConnections()){
con.send(object, mode);
}
}else{
provider.sendClient(object, mode);
}
}
/** Send an object to everyone EXCEPT a certain client. Server-side only.*/
public void sendExcept(NetConnection except, Object object, SendMode mode){
for(NetConnection con : getConnections()){
if(con != except){
con.send(object, mode);
}
}
}
public @Nullable StreamBuilder getCurrentStream(){
return currentStream;
}
/**
* Registers a client listener for when an object is recieved.
*/
public <T> void handleClient(Class<T> type, Cons<T> listener){
clientListeners.put(type, listener);
}
/**
* Registers a server listener for when an object is recieved.
*/
public <T> void handleServer(Class<T> type, Cons2<NetConnection, T> listener){
serverListeners.put(type, (Cons2<NetConnection, Object>)listener);
}
/**
* Call to handle a packet being recieved for the client.
*/
public void handleClientReceived(Object object){
if(object instanceof StreamBegin){
StreamBegin b = (StreamBegin)object;
streams.put(b.id, currentStream = new StreamBuilder(b));
}else if(object instanceof StreamChunk){
StreamChunk c = (StreamChunk)object;
StreamBuilder builder = streams.get(c.id);
if(builder == null){
throw new RuntimeException("Recieved stream chunk without a StreamBegin beforehand!");
}
builder.add(c.data);
if(builder.isDone()){
streams.remove(builder.id);
handleClientReceived(builder.build());
currentStream = null;
}
}else if(clientListeners.get(object.getClass()) != null){
if(clientLoaded || ((object instanceof Packet) && ((Packet)object).isImportant())){
if(clientListeners.get(object.getClass()) != null)
clientListeners.get(object.getClass()).get(object);
Pools.free(object);
}else if(!((object instanceof Packet) && ((Packet)object).isUnimportant())){
packetQueue.add(object);
}else{
Pools.free(object);
}
}else{
Log.err("Unhandled packet type: '{0}'!", object);
}
}
/**
* Call to handle a packet being recieved for the server.
*/
public void handleServerReceived(NetConnection connection, Object object){
if(serverListeners.get(object.getClass()) != null){
if(serverListeners.get(object.getClass()) != null)
serverListeners.get(object.getClass()).get(connection, object);
Pools.free(object);
}else{
Log.err("Unhandled packet type: '{0}'!", object.getClass());
}
}
/**
* Pings a host in an new thread. If an error occured, failed() should be called with the exception.
*/
public void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed){
provider.pingHost(address, port, valid, failed);
}
/**
* Whether the net is active, e.g. whether this is a multiplayer game.
*/
public boolean active(){
return active;
}
/**
* Whether this is a server or not.
*/
public boolean server(){
return server && active;
}
/**
* Whether this is a client or not.
*/
public boolean client(){
return !server && active;
}
public void dispose(){
provider.dispose();
server = false;
active = false;
}
public enum SendMode{
tcp, udp
}
/** Networking implementation. */
public interface NetProvider{
/** Connect to a server. */
void connectClient(String ip, int port, Runnable success) throws IOException;
/** Send an object to the server. */
void sendClient(Object object, SendMode mode);
/** Disconnect from the server. */
void disconnectClient();
/**
* Discover servers. This should run the callback regardless of whether any servers are found. Should not block.
* Callback should be run on the main thread.
* @param done is the callback that should run after discovery.
*/
void discoverServers(Cons<Host> callback, Runnable done);
/** Ping a host. If an error occured, failed() should be called with the exception. */
void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed);
/** Host a server at specified port. */
void hostServer(int port) throws IOException;
/** Return all connected users. */
Iterable<? extends NetConnection> getConnections();
/** Close the server connection. */
void closeServer();
/** Close all connections. */
default void dispose(){
disconnectClient();
closeServer();
}
}
}

View File

@@ -0,0 +1,97 @@
package mindustry.net;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import mindustry.entities.type.*;
import mindustry.gen.*;
import mindustry.net.Administration.*;
import mindustry.net.Net.*;
import mindustry.net.Packets.*;
import java.io.*;
import static mindustry.Vars.netServer;
public abstract class NetConnection{
public final String address;
public boolean mobile, modclient;
public @Nullable
Player player;
/** ID of last recieved client snapshot. */
public int lastRecievedClientSnapshot = -1;
/** Timestamp of last recieved snapshot. */
public long lastRecievedClientTime;
public boolean hasConnected, hasBegunConnecting, hasDisconnected;
public float viewWidth, viewHeight, viewX, viewY;
public NetConnection(String address){
this.address = address;
}
/** Kick with a special, localized reason. Use this if possible. */
public void kick(KickReason reason){
Log.info("Kicking connection {0}; Reason: {1}", address, reason.name());
if(player != null && (reason == KickReason.kick || reason == KickReason.banned || reason == KickReason.vote) && player.uuid != null){
PlayerInfo info = netServer.admins.getInfo(player.uuid);
info.timesKicked++;
info.lastKicked = Math.max(Time.millis(), info.lastKicked);
}
Call.onKick(this, reason);
Time.runTask(2f, this::close);
netServer.admins.save();
}
/** Kick with an arbitrary reason. */
public void kick(String reason){
Log.info("Kicking connection {0}; Reason: {1}", address, reason.replace("\n", " "));
if(player != null && player.uuid != null){
PlayerInfo info = netServer.admins.getInfo(player.uuid);
info.timesKicked++;
info.lastKicked = Math.max(Time.millis(), info.lastKicked);
}
Call.onKick(this, reason);
Time.runTask(2f, this::close);
netServer.admins.save();
}
public boolean isConnected(){
return true;
}
public void sendStream(Streamable stream){
try{
int cid;
StreamBegin begin = new StreamBegin();
begin.total = stream.stream.available();
begin.type = Registrator.getID(stream.getClass());
send(begin, SendMode.tcp);
cid = begin.id;
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;
send(chunk, SendMode.tcp);
}
}catch(IOException e){
throw new RuntimeException(e);
}
}
public abstract void send(Object object, SendMode mode);
public abstract void close();
}

View File

@@ -0,0 +1,117 @@
package mindustry.net;
import arc.*;
import arc.util.*;
import mindustry.core.*;
import mindustry.entities.type.*;
import mindustry.game.*;
import mindustry.io.*;
import mindustry.maps.Map;
import java.io.*;
import java.nio.*;
import java.util.*;
import static mindustry.Vars.*;
public class NetworkIO{
public static void writeWorld(Player player, OutputStream os){
try(DataOutputStream stream = new DataOutputStream(os)){
stream.writeUTF(JsonIO.write(state.rules));
SaveIO.getSaveWriter().writeStringMap(stream, world.getMap().tags);
stream.writeInt(state.wave);
stream.writeFloat(state.wavetime);
stream.writeInt(player.id);
player.write(stream);
SaveIO.getSaveWriter().writeContentHeader(stream);
SaveIO.getSaveWriter().writeMap(stream);
}catch(IOException e){
throw new RuntimeException(e);
}
}
public static void loadWorld(InputStream is){
try(DataInputStream stream = new DataInputStream(is)){
Time.clear();
state.rules = JsonIO.read(Rules.class, stream.readUTF());
world.setMap(new Map(SaveIO.getSaveWriter().readStringMap(stream)));
state.wave = stream.readInt();
state.wavetime = stream.readFloat();
entities.clear();
int id = stream.readInt();
player.resetNoAdd();
player.read(stream);
player.resetID(id);
player.add();
SaveIO.getSaveWriter().readContentHeader(stream);
SaveIO.getSaveWriter().readMap(stream, world.context);
}catch(IOException e){
throw new RuntimeException(e);
}finally{
content.setTemporaryMapper(null);
}
}
public static ByteBuffer writeServerData(){
String name = (headless ? Core.settings.getString("servername") : player.name);
String map = world.getMap() == null ? "None" : world.getMap().name();
ByteBuffer buffer = ByteBuffer.allocate(256);
writeString(buffer, name, 100);
writeString(buffer, map);
buffer.putInt(playerGroup.size());
buffer.putInt(state.wave);
buffer.putInt(Version.build);
writeString(buffer, Version.type);
buffer.put((byte)Gamemode.bestFit(state.rules).ordinal());
buffer.putInt(netServer.admins.getPlayerLimit());
return buffer;
}
public static Host readServerData(String hostAddress, ByteBuffer buffer){
String host = readString(buffer);
String map = readString(buffer);
int players = buffer.getInt();
int wave = buffer.getInt();
int version = buffer.getInt();
String vertype = readString(buffer);
Gamemode gamemode = Gamemode.all[buffer.get()];
int limit = buffer.getInt();
return new Host(host, hostAddress, map, wave, players, version, vertype, gamemode, limit);
}
private static void writeString(ByteBuffer buffer, String string, int maxlen){
byte[] bytes = string.getBytes(charset);
//todo truncating this way may lead to wierd encoding errors at the ends of strings...
if(bytes.length > maxlen){
bytes = Arrays.copyOfRange(bytes, 0, maxlen);
}
buffer.put((byte)bytes.length);
buffer.put(bytes);
}
private static void writeString(ByteBuffer buffer, String string){
writeString(buffer, string, 32);
}
private static String readString(ByteBuffer buffer){
short length = (short)(buffer.get() & 0xff);
byte[] bytes = new byte[length];
buffer.get(bytes);
return new String(bytes, charset);
}
}

View File

@@ -0,0 +1,24 @@
package mindustry.net;
import arc.util.pooling.Pool.Poolable;
import java.nio.ByteBuffer;
public interface Packet extends Poolable{
default void read(ByteBuffer buffer){
}
default void write(ByteBuffer buffer){
}
default void reset(){
}
default boolean isImportant(){
return false;
}
default boolean isUnimportant(){
return false;
}
}

View File

@@ -0,0 +1,193 @@
package mindustry.net;
import arc.*;
import arc.struct.*;
import arc.util.serialization.*;
import mindustry.core.*;
import mindustry.io.*;
import java.nio.*;
/**
* Class for storing all packets.
*/
public class Packets{
public enum KickReason{
kick, clientOutdated, serverOutdated, banned, gameover(true), recentKick,
nameInUse, idInUse, nameEmpty, customClient, serverClose, vote, typeMismatch, whitelist, playerLimit;
public final boolean quiet;
KickReason(){
this(false);
}
KickReason(boolean quiet){
this.quiet = quiet;
}
@Override
public String toString(){
return Core.bundle.get("server.kicked." + name());
}
public String extraText(){
return Core.bundle.getOrNull("server.kicked." + name() + ".text");
}
}
public enum AdminAction{
kick, ban, trace, wave
}
public static class Connect implements Packet{
public String addressTCP;
@Override
public boolean isImportant(){
return true;
}
}
public static class Disconnect implements Packet{
public String reason;
@Override
public boolean isImportant(){
return true;
}
}
public static class WorldStream extends Streamable{
}
public static class InvokePacket implements Packet{
public byte type, priority;
public ByteBuffer writeBuffer;
public int writeLength;
@Override
public void read(ByteBuffer buffer){
type = buffer.get();
priority = buffer.get();
writeLength = buffer.getShort();
byte[] bytes = new byte[writeLength];
buffer.get(bytes);
writeBuffer = ByteBuffer.wrap(bytes);
}
@Override
public void write(ByteBuffer buffer){
buffer.put(type);
buffer.put(priority);
buffer.putShort((short)writeLength);
writeBuffer.position(0);
for(int i = 0; i < writeLength; i++){
buffer.put(writeBuffer.get());
}
}
@Override
public void reset(){
priority = 0;
}
@Override
public boolean isImportant(){
return priority == 1;
}
@Override
public boolean isUnimportant(){
return priority == 2;
}
}
/** Marks the beginning of a stream. */
public static class StreamBegin implements Packet{
private static int lastid;
public int id = lastid++;
public int total;
public byte type;
@Override
public void write(ByteBuffer buffer){
buffer.putInt(id);
buffer.putInt(total);
buffer.put(type);
}
@Override
public void read(ByteBuffer buffer){
id = buffer.getInt();
total = buffer.getInt();
type = buffer.get();
}
}
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 ConnectPacket implements Packet{
public int version;
public String versionType;
public Array<String> mods;
public String name, uuid, usid;
public boolean mobile;
public int color;
@Override
public void write(ByteBuffer buffer){
buffer.putInt(Version.build);
TypeIO.writeString(buffer, versionType);
TypeIO.writeString(buffer, name);
TypeIO.writeString(buffer, usid);
buffer.put(mobile ? (byte)1 : 0);
buffer.put(Base64Coder.decode(uuid));
buffer.put((byte)color);
buffer.put((byte)mods.size);
for(int i = 0; i < mods.size; i++){
TypeIO.writeString(buffer, mods.get(i));
}
}
@Override
public void read(ByteBuffer buffer){
version = buffer.getInt();
versionType = TypeIO.readString(buffer);
name = TypeIO.readString(buffer);
usid = TypeIO.readString(buffer);
mobile = buffer.get() == 1;
color = buffer.getInt();
byte[] idbytes = new byte[8];
buffer.get(idbytes);
uuid = new String(Base64Coder.encode(idbytes));
int totalMods = buffer.get();
mods = new Array<>(totalMods);
for(int i = 0; i < totalMods; i++){
mods.add(TypeIO.readString(buffer));
}
}
}
}

View File

@@ -0,0 +1,45 @@
package mindustry.net;
import arc.struct.ObjectIntMap;
import arc.func.Prov;
import mindustry.net.Packets.*;
public class Registrator{
private static ClassEntry[] classes = {
new ClassEntry(StreamBegin.class, StreamBegin::new),
new ClassEntry(StreamChunk.class, StreamChunk::new),
new ClassEntry(WorldStream.class, WorldStream::new),
new ClassEntry(ConnectPacket.class, ConnectPacket::new),
new ClassEntry(InvokePacket.class, InvokePacket::new)
};
private static ObjectIntMap<Class> 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++){
ids.put(classes[i].type, i);
}
}
public static ClassEntry getByID(byte id){
return classes[id];
}
public static byte getID(Class<?> type){
return (byte)ids.get(type, -1);
}
public static ClassEntry[] getClasses(){
return classes;
}
public static class ClassEntry{
public final Class<?> type;
public final Prov<?> constructor;
public <T extends Packet> ClassEntry(Class<T> type, Prov<T> constructor){
this.type = type;
this.constructor = constructor;
}
}
}

View File

@@ -0,0 +1,49 @@
package mindustry.net;
import mindustry.net.Packets.StreamBegin;
import java.io.*;
public class Streamable implements Packet{
public transient ByteArrayInputStream stream;
@Override
public boolean isImportant(){
return true;
}
public static class StreamBuilder{
public final int id;
public final byte type;
public final int total;
public final ByteArrayOutputStream stream = new ByteArrayOutputStream();
public StreamBuilder(StreamBegin begin){
id = begin.id;
type = begin.type;
total = begin.total;
}
public float progress(){
return (float)stream.size() / total;
}
public void add(byte[] bytes){
try{
stream.write(bytes);
}catch(IOException e){
throw new RuntimeException(e);
}
}
public Streamable build(){
Streamable s = (Streamable)Registrator.getByID(type).constructor.get();
s.stream = new ByteArrayInputStream(stream.toByteArray());
return s;
}
public boolean isDone(){
return stream.size() >= total;
}
}
}

View File

@@ -0,0 +1,15 @@
package mindustry.net;
import mindustry.entities.type.Player;
/**
* Thrown when a client sends invalid information.
*/
public class ValidateException extends RuntimeException{
public final Player player;
public ValidateException(Player player, String s){
super(s);
this.player = player;
}
}