it is done
This commit is contained in:
349
core/src/mindustry/net/Administration.java
Normal file
349
core/src/mindustry/net/Administration.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
426
core/src/mindustry/net/ArcNetProvider.java
Normal file
426
core/src/mindustry/net/ArcNetProvider.java
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
158
core/src/mindustry/net/CrashSender.java
Normal file
158
core/src/mindustry/net/CrashSender.java
Normal 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){
|
||||
}
|
||||
}
|
||||
}
|
||||
27
core/src/mindustry/net/Host.java
Normal file
27
core/src/mindustry/net/Host.java
Normal 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;
|
||||
}
|
||||
}
|
||||
68
core/src/mindustry/net/Interpolator.java
Normal file
68
core/src/mindustry/net/Interpolator.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
347
core/src/mindustry/net/Net.java
Normal file
347
core/src/mindustry/net/Net.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
97
core/src/mindustry/net/NetConnection.java
Normal file
97
core/src/mindustry/net/NetConnection.java
Normal 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();
|
||||
}
|
||||
117
core/src/mindustry/net/NetworkIO.java
Normal file
117
core/src/mindustry/net/NetworkIO.java
Normal 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);
|
||||
}
|
||||
}
|
||||
24
core/src/mindustry/net/Packet.java
Normal file
24
core/src/mindustry/net/Packet.java
Normal 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;
|
||||
}
|
||||
}
|
||||
193
core/src/mindustry/net/Packets.java
Normal file
193
core/src/mindustry/net/Packets.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
core/src/mindustry/net/Registrator.java
Normal file
45
core/src/mindustry/net/Registrator.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
core/src/mindustry/net/Streamable.java
Normal file
49
core/src/mindustry/net/Streamable.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
core/src/mindustry/net/ValidateException.java
Normal file
15
core/src/mindustry/net/ValidateException.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user