package mindustry.net; import arc.*; import arc.func.*; import arc.net.*; import arc.struct.*; import arc.util.*; 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 java.nio.channels.*; import static mindustry.Vars.*; @SuppressWarnings("unchecked") public class Net{ private boolean server; private boolean active; private boolean clientLoaded; private @Nullable StreamBuilder currentStream; private final Seq packetQueue = new Seq<>(); private final ObjectMap, Cons> clientListeners = new ObjectMap<>(); private final ObjectMap, Cons2> serverListeners = new ObjectMap<>(); private final IntMap 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; } public void handleException(Throwable e){ if(e instanceof ArcNetException){ Core.app.post(() -> showError(new IOException("mismatch"))); }else if(e instanceof ClosedChannelException){ Core.app.post(() -> showError(new IOException("alreadyconnected"))); }else{ Core.app.post(() -> showError(e)); } } /** 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.neatError(e).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 receive 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.kick(con, KickReason.serverClose); } provider.closeServer(); server = false; active = false; } public void reset(){ closeServer(); netClient.disconnectNoReset(); } public void disconnect(){ if(active && !server){ Log.info("Disconnecting."); } 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 Arc thread. */ public void discoverServers(Cons cons, Runnable done){ provider.discoverServers(cons, done); } /** * Returns a list of all connections IDs. */ public Iterable getConnections(){ return (Iterable)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 received. */ public void handleClient(Class type, Cons listener){ clientListeners.put(type, listener); } /** * Registers a server listener for when an object is received. */ public void handleServer(Class type, Cons2 listener){ serverListeners.put(type, (Cons2)listener); } /** * Call to handle a packet being received for the client. */ public void handleClientReceived(Object object){ if(object instanceof StreamBegin b){ streams.put(b.id, currentStream = new StreamBuilder(b)); }else if(object instanceof StreamChunk c){ StreamBuilder builder = streams.get(c.id); if(builder == null){ throw new RuntimeException("Received 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: '@'!", object); } } /** * Call to handle a packet being received 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: '@'!", 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 valid, Cons 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 callback, Runnable done); /** Ping a host. If an error occured, failed() should be called with the exception. */ void pingHost(String address, int port, Cons valid, Cons failed); /** Host a server at specified port. */ void hostServer(int port) throws IOException; /** Return all connected users. */ Iterable getConnections(); /** Close the server connection. */ void closeServer(); /** Close all connections. */ default void dispose(){ disconnectClient(); closeServer(); } } }