Merge branch 'master' into port-field

This commit is contained in:
Anuken
2025-02-08 19:36:26 -05:00
committed by GitHub
2681 changed files with 151338 additions and 45271 deletions

View File

@@ -11,6 +11,7 @@ import mindustry.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.payloads.*;
import static mindustry.Vars.*;
import static mindustry.game.EventType.*;
@@ -21,17 +22,20 @@ public class Administration{
public Seq<ChatFilter> chatFilters = new Seq<>();
public Seq<ActionFilter> actionFilters = new Seq<>();
public Seq<String> subnetBans = new Seq<>();
public ObjectSet<String> dosBlacklist = new ObjectSet<>();
public ObjectMap<String, Long> kickedIPs = new ObjectMap<>();
/** All player info. Maps UUIDs to info. This persists throughout restarts. Do not access directly. */
private ObjectMap<String, PlayerInfo> playerInfo = new ObjectMap<>();
private boolean modified, loaded;
/** All player info. Maps UUIDs to info. This persists throughout restarts. Do not modify directly. */
public ObjectMap<String, PlayerInfo> playerInfo = new ObjectMap<>();
public Administration(){
load();
//anti-spam
addChatFilter((player, message) -> {
long resetTime = Config.messageRateLimit.num() * 1000;
long resetTime = Config.messageRateLimit.num() * 1000L;
if(Config.antiSpam.bool() && !player.isLocal() && !player.admin){
//prevent people from spamming messages quickly
if(resetTime > 0 && Time.timeSinceMillis(player.getInfo().lastMessageTime) < resetTime){
@@ -47,8 +51,8 @@ public class Administration{
player.getInfo().messageInfractions = 0;
}
//prevent players from sending the same message twice in the span of 50 seconds
if(message.equals(player.getInfo().lastSentMessage) && Time.timeSinceMillis(player.getInfo().lastMessageTime) < 1000 * 50){
//prevent players from sending the same message twice in the span of 10 seconds
if(message.equals(player.getInfo().lastSentMessage) && Time.timeSinceMillis(player.getInfo().lastMessageTime) < 1000 * 10){
player.sendMessage("[scarlet]You may not send the same message twice.");
return null;
}
@@ -61,14 +65,14 @@ public class Administration{
});
//block interaction rate limit
//TODO when someone disconnects, a different player is mistakenly kicked for spamming actions
addActionFilter(action -> {
if(action.type != ActionType.breakBlock &&
action.type != ActionType.placeBlock &&
Config.antiSpam.bool()){
action.type != ActionType.commandUnits &&
Config.antiSpam.bool() && !action.player.isLocal()){
Ratekeeper rate = action.player.getInfo().rate;
if(rate.allow(Config.interactRateWindow.num() * 1000, Config.interactRateLimit.num())){
if(rate.allow(Config.interactRateWindow.num() * 1000L, Config.interactRateLimit.num())){
return true;
}else{
if(rate.occurences > Config.interactRateKick.num()){
@@ -84,6 +88,14 @@ public class Administration{
});
}
public synchronized void blacklistDos(String address){
dosBlacklist.add(address);
}
public synchronized boolean isDosBlacklisted(String address){
return dosBlacklist.contains(address);
}
/** @return time at which a player would be pardoned for a kick (0 means they were never kicked) */
public long getKickTime(String uuid, String ip){
return Math.max(getInfo(uuid).lastKicked, kickedIPs.get(ip, 0L));
@@ -163,7 +175,7 @@ public class Administration{
}
public int getPlayerLimit(){
return Core.settings.getInt("playerlimit", 0);
return Core.settings.getInt("playerlimit", headless ? 30 : 0);
}
public void setPlayerLimit(int limit){
@@ -220,7 +232,7 @@ public class Administration{
getCreateInfo(id).banned = true;
save();
Events.fire(new PlayerBanEvent(Groups.player.find(p -> id.equals(p.uuid()))));
Events.fire(new PlayerBanEvent(Groups.player.find(p -> id.equals(p.uuid())), id));
return true;
}
@@ -259,7 +271,7 @@ public class Administration{
info.banned = false;
bannedIPs.removeAll(info.ips, false);
save();
Events.fire(new PlayerUnbanEvent(Groups.player.find(p -> id.equals(p.uuid()))));
Events.fire(new PlayerUnbanEvent(Groups.player.find(p -> id.equals(p.uuid())), id));
return true;
}
@@ -303,11 +315,13 @@ public class Administration{
public boolean adminPlayer(String id, String usid){
PlayerInfo info = getCreateInfo(id);
var wasAdmin = info.admin;
info.adminUsid = usid;
info.admin = true;
save();
return true;
return wasAdmin;
}
/**
@@ -369,7 +383,7 @@ public class Administration{
ObjectSet<PlayerInfo> result = new ObjectSet<>();
for(PlayerInfo info : playerInfo.values()){
if(info.lastName.equalsIgnoreCase(name) || (info.names.contains(name, false))
if(info.lastName.equalsIgnoreCase(name) || info.names.contains(name, false)
|| Strings.stripColors(Strings.stripColors(info.lastName)).equals(name)
|| info.ips.contains(name, false) || info.id.equals(name)){
result.add(info);
@@ -437,75 +451,97 @@ public class Administration{
}
public void save(){
Core.settings.putJson("player-data", playerInfo);
Core.settings.putJson("ip-bans", String.class, bannedIPs);
Core.settings.putJson("whitelist-ids", String.class, whitelist);
Core.settings.putJson("banned-subnets", String.class, subnetBans);
modified = true;
}
public void forceSave(){
if(modified && loaded){
Core.settings.putJson("player-data", playerInfo);
Core.settings.putJson("ip-kicks", kickedIPs);
Core.settings.putJson("ip-bans", String.class, bannedIPs);
Core.settings.putJson("whitelist-ids", String.class, whitelist);
Core.settings.putJson("banned-subnets", String.class, subnetBans);
modified = false;
}
}
@SuppressWarnings("unchecked")
private void load(){
loaded = true;
//load default data
playerInfo = Core.settings.getJson("player-data", ObjectMap.class, ObjectMap::new);
kickedIPs = Core.settings.getJson("ip-kicks", ObjectMap.class, ObjectMap::new);
bannedIPs = Core.settings.getJson("ip-bans", Seq.class, Seq::new);
whitelist = Core.settings.getJson("whitelist-ids", Seq.class, Seq::new);
subnetBans = Core.settings.getJson("banned-subnets", Seq.class, Seq::new);
}
/** Server configuration definition. Each config value can be a string, boolean or number. */
public enum Config{
name("The server name as displayed on clients.", "Server", "servername"),
desc("The server description, displayed under the name. Max 100 characters.", "off"),
port("The port to host on.", Vars.port),
autoUpdate("Whether to auto-update and exit when a new bleeding-edge update arrives.", false),
showConnectMessages("Whether to display connect/disconnect messages.", true),
enableVotekick("Whether votekick is enabled.", true),
startCommands("Commands run at startup. This should be a comma-separated list.", ""),
crashReport("Whether to send crash reports.", false, "crashreport"),
logging("Whether to log everything to files.", true),
strict("Whether strict mode is on - corrects positions and prevents duplicate UUIDs.", true),
antiSpam("Whether spammers are automatically kicked and rate-limited.", headless),
interactRateWindow("Block interaction rate limit window, in seconds.", 6),
interactRateLimit("Block interaction rate limit.", 25),
interactRateKick("How many times a player must interact inside the window to get kicked.", 60),
messageRateLimit("Message rate limit in seconds. 0 to disable.", 0),
messageSpamKick("How many times a player must send a message before the cooldown to get kicked. 0 to disable.", 3),
socketInput("Allows a local application to control this server through a local TCP socket.", false, "socket", () -> Events.fire(Trigger.socketConfigChanged)),
socketInputPort("The port for socket input.", 6859, () -> Events.fire(Trigger.socketConfigChanged)),
socketInputAddress("The bind address for socket input.", "localhost", () -> Events.fire(Trigger.socketConfigChanged)),
allowCustomClients("Whether custom clients are allowed to connect.", !headless, "allow-custom"),
whitelist("Whether the whitelist is used.", false),
motd("The message displayed to people on connection.", "off"),
autosave("Whether the periodically save the map when playing.", false),
autosaveAmount("The maximum amount of autosaves. Older ones get replaced.", 10),
autosaveSpacing("Spacing between autosaves in seconds.", 60 * 5),
debug("Enable debug logging", false, () -> {
Log.level = debug() ? LogLevel.debug : LogLevel.info;
});
/**
* Server configuration definition. Each config value can be a string, boolean or number.
* Creating a new Config instance implicitly adds it to the list of server configs. This can be used for custom plugin configuration.
* */
public static class Config{
public static final Seq<Config> all = new Seq<>();
public static final Config[] all = values();
public static final Config
serverName = new Config("name", "The server name as displayed on clients.", "Server", "servername"),
desc = new Config("desc", "The server description, displayed under the name. Max 100 characters.", "off"),
port = new Config("port", "The port to host on.", Vars.port),
autoUpdate = new Config("autoUpdate", "Whether to auto-update and exit when a new bleeding-edge update arrives.", false),
showConnectMessages = new Config("showConnectMessages", "Whether to display connect/disconnect messages.", true),
enableVotekick = new Config("enableVotekick", "Whether votekick is enabled.", true),
startCommands = new Config("startCommands", "Commands run at startup. This should be a comma-separated list.", ""),
logging = new Config("logging", "Whether to log everything to files.", true),
strict = new Config("strict", "Whether strict mode is on - corrects positions and prevents duplicate UUIDs.", true),
antiSpam = new Config("antiSpam", "Whether spammers are automatically kicked and rate-limited.", headless),
interactRateWindow = new Config("interactRateWindow", "Block interaction rate limit window, in seconds.", 6),
interactRateLimit = new Config("interactRateLimit", "Block interaction rate limit.", 25),
interactRateKick = new Config("interactRateKick", "How many times a player must interact inside the window to get kicked.", 60),
messageRateLimit = new Config("messageRateLimit", "Message rate limit in seconds. 0 to disable.", 0),
messageSpamKick = new Config("messageSpamKick", "How many times a player must send a message before the cooldown to get kicked. 0 to disable.", 3),
packetSpamLimit = new Config("packetSpamLimit", "Limit for packet count sent within 3sec that will lead to a blacklist + kick.", 300),
chatSpamLimit = new Config("chatSpamLimit", "Limit for chat packet count sent within 2sec that will lead to a blacklist + kick. Not the same as a rate limit.", 20),
socketInput = new Config("socketInput", "Allows a local application to control this server through a local TCP socket.", false, "socket", () -> Events.fire(Trigger.socketConfigChanged)),
socketInputPort = new Config("socketInputPort", "The port for socket input.", 6859, () -> Events.fire(Trigger.socketConfigChanged)),
socketInputAddress = new Config("socketInputAddress", "The bind address for socket input.", "localhost", () -> Events.fire(Trigger.socketConfigChanged)),
allowCustomClients = new Config("allowCustomClients", "Whether custom clients are allowed to connect.", !headless, "allow-custom"),
whitelist = new Config("whitelist", "Whether the whitelist is used.", false),
motd = new Config("motd", "The message displayed to people on connection.", "off"),
autosave = new Config("autosave", "Whether the periodically save the map when playing.", false),
autosaveAmount = new Config("autosaveAmount", "The maximum amount of autosaves. Older ones get replaced.", 10),
autosaveSpacing = new Config("autosaveSpacing", "Spacing between autosaves in seconds.", 60 * 5),
debug = new Config("debug", "Enable debug logging.", false, () -> Log.level = debug() ? LogLevel.debug : LogLevel.info),
snapshotInterval = new Config("snapshotInterval", "Client entity snapshot interval in ms.", 200),
autoPause = new Config("autoPause", "Whether the game should pause when nobody is online.", false),
roundExtraTime = new Config("roundExtraTime", "Time before loading a new map after the gameover, in seconds.", 12),
maxLogLength = new Config("maxLogLength", "The Maximum log file size, in bytes.", 1024 * 1024 * 5);
public final Object defaultValue;
public final String key, description;
public final String name, key, description;
final Runnable changed;
Config(String description, Object def){
this(description, def, null, null);
public Config(String name, String description, Object def){
this(name, description, def, null, null);
}
Config(String description, Object def, String key){
this(description, def, key, null);
public Config(String name, String description, Object def, String key){
this(name, description, def, key, null);
}
Config(String description, Object def, Runnable changed){
this(description, def, null, changed);
public Config(String name, String description, Object def, Runnable changed){
this(name, description, def, null, changed);
}
Config(String description, Object def, String key, Runnable changed){
public Config(String name, String description, Object def, String key, Runnable changed){
this.name = name;
this.description = description;
this.key = key == null ? name() : key;
this.key = key == null ? name : key;
this.defaultValue = def;
this.changed = changed == null ? () -> {} : changed;
all.add(this);
}
public boolean isNum(){
@@ -541,6 +577,10 @@ public class Administration{
changed.run();
}
public boolean isDefault(){
return Structs.eq(get(), defaultValue);
}
private static boolean debug(){
return Config.debug.bool();
}
@@ -569,6 +609,10 @@ public class Administration{
public PlayerInfo(){
}
public String plainLastName(){
return Strings.stripColors(lastName);
}
}
/** Handles chat messages from players and changes their contents. */
@@ -584,14 +628,21 @@ public class Administration{
}
public static class TraceInfo{
public String ip, uuid;
public String ip, uuid, locale;
public boolean modded, mobile;
public int timesJoined, timesKicked;
public String[] ips, names;
public TraceInfo(String ip, String uuid, boolean modded, boolean mobile){
public TraceInfo(String ip, String uuid, String locale, boolean modded, boolean mobile, int timesJoined, int timesKicked, String[] ips, String[] names){
this.ip = ip;
this.uuid = uuid;
this.locale = locale;
this.modded = modded;
this.mobile = mobile;
this.timesJoined = timesJoined;
this.timesKicked = timesKicked;
this.names = names;
this.ips = ips;
}
}
@@ -616,6 +667,18 @@ public class Administration{
/** valid for unit-type events only, and even in that case may be null. */
public @Nullable Unit unit;
/** valid only for payload events */
public @Nullable Payload payload;
/** valid only for removePlanned events only; contains packed positions. */
public @Nullable int[] plans;
/** valid only for command unit events */
public @Nullable int[] unitIDs;
/** valid only for command building events */
public @Nullable int[] buildingPositions;
public PlayerAction set(Player player, ActionType type, Tile tile){
this.player = player;
this.type = type;
@@ -640,11 +703,12 @@ public class Administration{
tile = null;
block = null;
unit = null;
plans = null;
}
}
public enum ActionType{
breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, command
breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned, commandUnits, commandBuilding, respawn, pickupBlock, dropPayload
}
}

View File

@@ -2,14 +2,21 @@ package mindustry.net;
import arc.*;
import arc.func.*;
import arc.math.*;
import arc.net.*;
import arc.net.FrameworkMessage.*;
import arc.net.Server.*;
import arc.net.dns.*;
import arc.struct.*;
import arc.util.*;
import arc.util.async.*;
import arc.util.pooling.*;
import arc.util.Log.*;
import arc.util.io.*;
import mindustry.*;
import mindustry.game.EventType.*;
import mindustry.net.Administration.*;
import mindustry.net.Net.*;
import mindustry.net.Packets.*;
import net.jpountz.lz4.*;
import java.io.*;
import java.net.*;
@@ -27,10 +34,30 @@ public class ArcNetProvider implements NetProvider{
final CopyOnWriteArrayList<ArcConnection> connections = new CopyOnWriteArrayList<>();
Thread serverThread;
public ArcNetProvider(){
ArcNet.errorHandler = e -> Log.debug(Strings.getStackTrace(e));
private static final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor();
private static final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor();
client = new Client(8192, 8192, new PacketSerializer());
private volatile int playerLimitCache, packetSpamLimit;
public ArcNetProvider(){
ArcNet.errorHandler = e -> {
if(Log.level == LogLevel.debug){
var finalCause = Strings.getFinalCause(e);
//"connection is closed" is a pointless annoying error that should not be logged
if(!"Connection is closed.".equals(finalCause.getMessage())){
Log.debug(Strings.getStackTrace(e));
}
}
};
//fetch this in the main thread to prevent threading issues
Events.run(Trigger.update, () -> {
playerLimitCache = netServer.admins.getPlayerLimit();
packetSpamLimit = Config.packetSpamLimit.num();
});
client = new Client(8192, 16384, new PacketSerializer());
client.setDiscoveryPacket(packetSupplier);
client.addListener(new NetListener(){
@Override
@@ -55,11 +82,11 @@ public class ArcNetProvider implements NetProvider{
@Override
public void received(Connection connection, Object object){
if(object instanceof FrameworkMessage) return;
if(!(object instanceof Packet p)) return;
Core.app.post(() -> {
try{
net.handleClientReceived(object);
net.handleClientReceived(p);
}catch(Throwable e){
net.handleException(e);
}
@@ -68,11 +95,13 @@ public class ArcNetProvider implements NetProvider{
}
});
server = new Server(32768, 8192, new PacketSerializer());
server = new Server(32768, 16384, new PacketSerializer());
server.setMulticast(multicastGroup, multicastPort);
server.setDiscoveryHandler((address, handler) -> {
ByteBuffer buffer = NetworkIO.writeServerData();
int length = buffer.position();
buffer.position(0);
buffer.limit(length);
handler.respond(buffer);
});
@@ -82,6 +111,12 @@ public class ArcNetProvider implements NetProvider{
public void connected(Connection connection){
String ip = connection.getRemoteAddressTCP().getAddress().getHostAddress();
//kill connections above the limit to prevent spam
if((playerLimitCache > 0 && server.getConnections().length > playerLimitCache) || netServer.admins.isDosBlacklisted(ip)){
connection.close(DcReason.closed);
return;
}
ArcConnection kn = new ArcConnection(ip, connection);
Connect c = new Connect();
@@ -89,14 +124,14 @@ public class ArcNetProvider implements NetProvider{
Log.debug("&bReceived connection: @", c.addressTCP);
connection.setArbitraryData(kn);
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;
if(!(connection.getArbitraryData() instanceof ArcConnection k)) return;
Disconnect c = new Disconnect();
c.reason = reason.toString();
@@ -109,20 +144,38 @@ public class ArcNetProvider implements NetProvider{
@Override
public void received(Connection connection, Object object){
ArcConnection k = getByArcID(connection.getID());
if(object instanceof FrameworkMessage || k == null) return;
if(!(connection.getArbitraryData() instanceof ArcConnection k)) return;
if(packetSpamLimit > 0 && !k.packetRate.allow(3000, packetSpamLimit)){
Log.warn("Blacklisting IP '@' as potential DOS attack - packet spam.", k.address);
connection.close(DcReason.closed);
netServer.admins.blacklistDos(k.address);
return;
}
if(!(object instanceof Packet pack)) return;
Core.app.post(() -> {
try{
net.handleServerReceived(k, object);
net.handleServerReceived(k, pack);
}catch(Throwable e){
e.printStackTrace();
Log.err(e);
}
});
}
});
}
@Override
public void setConnectFilter(Server.ServerConnectFilter connectFilter){
server.setConnectFilter(connectFilter);
}
@Override
public @Nullable ServerConnectFilter getConnectFilter(){
return server.getConnectFilter();
}
private static boolean isLocal(InetAddress addr){
if(addr.isAnyLocalAddress() || addr.isLoopbackAddress()) return true;
@@ -151,7 +204,9 @@ public class ArcNetProvider implements NetProvider{
client.connect(5000, ip, port, port);
success.run();
}catch(Exception e){
net.handleException(e);
if(netClient.isConnecting()){
net.handleException(e);
}
}
});
}
@@ -162,9 +217,9 @@ public class ArcNetProvider implements NetProvider{
}
@Override
public void sendClient(Object object, SendMode mode){
public void sendClient(Object object, boolean reliable){
try{
if(mode == SendMode.tcp){
if(reliable){
client.sendTCP(object);
}else{
client.sendUDP(object);
@@ -173,51 +228,65 @@ public class ArcNetProvider implements NetProvider{
}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();
long time = Time.millis();
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((int)Time.timeSinceMillis(time), packet.getAddress().getHostAddress(), buffer);
Core.app.post(() -> valid.get(host));
}catch(Exception e){
Core.app.post(() -> invalid.get(e));
try{
var host = pingHostImpl(address, port);
Core.app.post(() -> valid.get(host));
}catch(IOException e){
if(port == Vars.port){
for(var record : ArcDns.getSrvRecords("_mindustry._tcp." + address)){
try{
var host = pingHostImpl(record.target, record.port);
Core.app.post(() -> valid.get(host));
return;
}catch(IOException ignored){
}
}
}
});
Core.app.post(() -> invalid.get(e));
}
}
private Host pingHostImpl(String address, int port) throws IOException{
try(DatagramSocket socket = new DatagramSocket()){
long time = Time.millis();
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((int)Time.timeSinceMillis(time), packet.getAddress().getHostAddress(), buffer);
host.port = port;
return host;
}
}
@Override
public void discoverServers(Cons<Host> callback, Runnable done){
Seq<InetAddress> foundAddresses = new Seq<>();
long time = Time.millis();
client.discoverHosts(port, multicastGroup, multicastPort, 3000, packet -> {
Core.app.post(() -> {
synchronized(foundAddresses){
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((int)Time.timeSinceMillis(time), packet.getAddress().getHostAddress(), buffer);
callback.get(host);
Core.app.post(() -> 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
//don't crash when there's an error pinging a server or parsing data
e.printStackTrace();
}
});
}
}, () -> Core.app.post(done));
}
@@ -255,18 +324,7 @@ public class ArcNetProvider implements NetProvider{
@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;
mainExecutor.submit(server::stop);
}
class ArcConnection extends NetConnection{
@@ -284,7 +342,7 @@ public class ArcNetProvider implements NetProvider{
@Override
public void sendStream(Streamable stream){
connection.addListener(new InputStreamSender(stream.stream, 512){
connection.addListener(new InputStreamSender(stream.stream, 1024){
int id;
@Override
@@ -292,7 +350,7 @@ public class ArcNetProvider implements NetProvider{
//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());
begin.type = Net.getPacketId(stream);
connection.sendTCP(begin);
id = begin.id;
}
@@ -308,20 +366,23 @@ public class ArcNetProvider implements NetProvider{
}
@Override
public void send(Object object, SendMode mode){
public void send(Object object, boolean reliable){
try{
if(mode == SendMode.tcp){
connection.sendTCP(object);
}else{
connection.sendUDP(object);
if(connection.isConnected()){
if(reliable){
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);
if(connection.getArbitraryData() instanceof ArcConnection k){
connections.remove(k);
}
}
}
@@ -331,32 +392,113 @@ public class ArcNetProvider implements NetProvider{
}
}
@SuppressWarnings("unchecked")
public static class PacketSerializer implements NetSerializer{
//for debugging total read/write speeds
private static final boolean debug = false;
ThreadLocal<ByteBuffer> decompressBuffer = Threads.local(() -> ByteBuffer.allocate(32768));
ThreadLocal<Reads> reads = Threads.local(() -> new Reads(new ByteBufferInput(decompressBuffer.get())));
ThreadLocal<Writes> writes = Threads.local(() -> new Writes(new ByteBufferOutput(decompressBuffer.get())));
//for debugging network write counts
static WindowedMean upload = new WindowedMean(5), download = new WindowedMean(5);
static long lastUpload, lastDownload, uploadAccum, downloadAccum;
static int lastPos;
@Override
public Object read(ByteBuffer byteBuffer){
if(debug){
if(Time.timeSinceMillis(lastDownload) >= 1000){
lastDownload = Time.millis();
download.add(downloadAccum);
downloadAccum = 0;
Log.info("Download: @ b/s", download.mean());
}
downloadAccum += byteBuffer.remaining();
}
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);
//read length int, followed by compressed lz4 data
Packet packet = Net.newPacket(id);
var buffer = decompressBuffer.get();
int length = byteBuffer.getShort() & 0xffff;
byte compression = byteBuffer.get();
//no compression, copy over buffer
if(compression == 0){
buffer.position(0).limit(length);
buffer.put(byteBuffer.array(), byteBuffer.position(), length);
buffer.position(0);
packet.read(reads.get(), length);
//move read packets forward
byteBuffer.position(byteBuffer.position() + buffer.position());
}else{
//decompress otherwise
int read = decompressor.decompress(byteBuffer, byteBuffer.position(), buffer, 0, length);
buffer.position(0);
buffer.limit(length);
packet.read(reads.get(), length);
//move buffer forward based on bytes read by decompressor
byteBuffer.position(byteBuffer.position() + read);
}
return packet;
}
}
@Override
public void write(ByteBuffer byteBuffer, Object o){
if(o instanceof FrameworkMessage){
if(debug){
lastPos = byteBuffer.position();
}
//write raw buffer
if(o instanceof ByteBuffer raw){
byteBuffer.put(raw);
}else if(o instanceof FrameworkMessage msg){
byteBuffer.put((byte)-2); //code for framework message
writeFramework(byteBuffer, (FrameworkMessage)o);
writeFramework(byteBuffer, msg);
}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());
if(!(o instanceof Packet pack)) throw new RuntimeException("All sent objects must extend Packet! Class: " + o.getClass());
byte id = Net.getPacketId(pack);
byteBuffer.put(id);
((Packet)o).write(byteBuffer);
var temp = decompressBuffer.get();
temp.position(0);
temp.limit(temp.capacity());
pack.write(writes.get());
short length = (short)temp.position();
//write length, uncompressed
byteBuffer.putShort(length);
//don't bother with small packets
if(length < 36 || pack instanceof StreamChunk){
//write direct contents...
byteBuffer.put((byte)0); //0 = no compression
byteBuffer.put(temp.array(), 0, length);
}else{
byteBuffer.put((byte)1); //1 = compression
//write compressed data; this does not modify position!
int written = compressor.compress(temp, 0, temp.position(), byteBuffer, byteBuffer.position(), byteBuffer.remaining());
//skip to indicate the written, compressed data
byteBuffer.position(byteBuffer.position() + written);
}
}
if(debug){
if(Time.timeSinceMillis(lastUpload) >= 1000){
lastUpload = Time.millis();
upload.add(uploadAccum);
uploadAccum = 0;
Log.info("Upload: @ b/s", upload.mean());
}
uploadAccum += byteBuffer.position() - lastPos;
}
}
@@ -387,9 +529,9 @@ public class ArcNetProvider implements NetProvider{
p.isReply = buffer.get() == 1;
return p;
}else if(id == 1){
return new DiscoverHost();
return FrameworkMessage.discoverHost;
}else if(id == 2){
return new KeepAlive();
return FrameworkMessage.keepAlive;
}else if(id == 3){
RegisterUDP p = new RegisterUDP();
p.connectionID = buffer.getInt();

View File

@@ -1,12 +1,11 @@
package mindustry.net;
import arc.*;
import arc.Net.*;
import arc.files.*;
import arc.func.*;
import arc.util.*;
import arc.util.async.*;
import arc.util.serialization.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.gen.*;
import mindustry.graphics.*;
@@ -25,7 +24,6 @@ import static mindustry.Vars.*;
public class BeControl{
private static final int updateInterval = 60;
private AsyncExecutor executor = new AsyncExecutor(1);
private boolean checkUpdates = true;
private boolean updateAvailable;
private String updateUrl;
@@ -33,21 +31,21 @@ public class BeControl{
/** @return whether this is a bleeding edge build. */
public boolean active(){
return Version.type.equals("bleeding-edge");
return Version.type.equals("bleeding-edge") && !steam;
}
public BeControl(){
if(active()){
Timer.schedule(() -> {
if(checkUpdates && !mobile){
if((Vars.clientLoaded || headless) && checkUpdates && !mobile){
checkUpdate(t -> {});
}
}, updateInterval, updateInterval);
}
if(System.getProperties().containsKey("becopy")){
if(OS.hasProp("becopy")){
try{
Fi dest = Fi.get(System.getProperty("becopy"));
Fi dest = Fi.get(OS.prop("becopy"));
Fi self = Fi.get(BeControl.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
for(Fi file : self.parent().findAll(f -> !f.equals(self))) file.delete();
@@ -61,27 +59,28 @@ public class BeControl{
/** asynchronously checks for updates. */
public void checkUpdate(Boolc done){
Core.net.httpGet("https://api.github.com/repos/Anuken/MindustryBuilds/releases/latest", res -> {
if(res.getStatus() == HttpStatus.OK){
Jval val = Jval.read(res.getResultAsString());
int newBuild = Strings.parseInt(val.getString("tag_name", "0"));
if(newBuild > Version.build){
Jval asset = val.get("assets").asArray().find(v -> v.getString("name", "").startsWith(headless ? "Mindustry-BE-Server" : "Mindustry-BE-Desktop"));
String url = asset.getString("browser_download_url", "");
updateAvailable = true;
updateBuild = newBuild;
updateUrl = url;
Core.app.post(() -> {
showUpdateDialog();
done.get(true);
});
}else{
Core.app.post(() -> done.get(false));
}
Http.get("https://api.github.com/repos/Anuken/MindustryBuilds/releases/latest")
.error(e -> {
//don't log the error, as it would clog output if there is no internet. make sure it's handled to prevent infinite loading.
done.get(false);
})
.submit(res -> {
Jval val = Jval.read(res.getResultAsString());
int newBuild = Strings.parseInt(val.getString("tag_name", "0"));
if(newBuild > Version.build){
Jval asset = val.get("assets").asArray().find(v -> v.getString("name", "").startsWith(headless ? "Mindustry-BE-Server" : "Mindustry-BE-Desktop"));
String url = asset.getString("browser_download_url", "");
updateAvailable = true;
updateBuild = newBuild;
updateUrl = url;
Core.app.post(() -> {
showUpdateDialog();
done.get(true);
});
}else{
Core.app.post(() -> done.get(false));
}
}, error -> {}); //ignore errors
});
}
/** @return whether a new update is available */
@@ -101,16 +100,16 @@ public class BeControl{
float[] progress = {0};
int[] length = {0};
Fi file = bebuildDirectory.child("client-be-" + updateBuild + ".jar");
Fi fileDest = System.getProperties().contains("becopy") ?
Fi.get(System.getProperty("becopy")) :
Fi fileDest = OS.hasProp("becopy") ?
Fi.get(OS.prop("becopy")) :
Fi.get(BeControl.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
BaseDialog dialog = new BaseDialog("@be.updating");
download(updateUrl, file, i -> length[0] = i, v -> progress[0] = v, () -> cancel[0], () -> {
try{
Runtime.getRuntime().exec(OS.isMac ?
new String[]{"java", "-XstartOnFirstThread", "-DlastBuild=" + Version.build, "-Dberestart", "-Dbecopy=" + fileDest.absolutePath(), "-jar", file.absolutePath()} :
new String[]{"java", "-DlastBuild=" + Version.build, "-Dberestart", "-Dbecopy=" + fileDest.absolutePath(), "-jar", file.absolutePath()}
new String[]{javaPath, "-XstartOnFirstThread", "-DlastBuild=" + Version.build, "-Dberestart", "-Dbecopy=" + fileDest.absolutePath(), "-jar", file.absolutePath()} :
new String[]{javaPath, "-DlastBuild=" + Version.build, "-Dberestart", "-Dbecopy=" + fileDest.absolutePath(), "-jar", file.absolutePath()}
);
System.exit(0);
}catch(IOException e){
@@ -152,7 +151,7 @@ public class BeControl{
Log.info("&lcAutosaved.");
netServer.kickAll(KickReason.serverRestarting);
Threads.sleep(32);
Threads.sleep(500);
Log.info("&lcVersion downloaded, exiting. Note that if you are not using a auto-restart script, the server will not restart automatically.");
//replace old file with new
@@ -170,7 +169,7 @@ public class BeControl{
}
private void download(String furl, Fi dest, Intc length, Floatc progressor, Boolp canceled, Runnable done, Cons<Throwable> error){
executor.submit(() -> {
mainExecutor.submit(() -> {
try{
HttpURLConnection con = (HttpURLConnection)new URL(furl).openConnection();
BufferedInputStream in = new BufferedInputStream(con.getInputStream());

View File

@@ -0,0 +1,154 @@
package mindustry.net;
import arc.*;
import arc.files.*;
import arc.func.*;
import arc.struct.*;
import arc.util.*;
import arc.util.io.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.mod.Mods.*;
import java.io.*;
import java.text.*;
import java.util.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class CrashHandler{
public static String createReport(Throwable exception){
String error = writeException(exception);
LoadedMod cause = getModCause(exception);
String report = cause == null ? "Mindustry has crashed. How unfortunate.\n" : "The mod '" + cause.meta.displayName + "' (" + cause.name + ")" + " has caused Mindustry to crash.\n";
if(mods != null && mods.list().size == 0 && Version.build != -1){
report += "Report this at " + Vars.reportIssueURL + "\n\n";
}
return report
+ "Version: " + Version.combined() + (Vars.headless ? " (Server)" : "") + "\n"
+ "OS: " + OS.osName + " x" + (OS.osArchBits) + " (" + OS.osArch + ")\n"
+ ((OS.isAndroid || OS.isIos) && app != null ? "Android API level: " + Core.app.getVersion() + "\n" : "")
+ "Java Version: " + OS.javaVersion + "\n"
+ "Runtime Available Memory: " + (Runtime.getRuntime().maxMemory() / 1024 / 1024) + "mb\n"
+ "Cores: " + OS.cores + "\n"
+ (cause == null ? "" : "Likely Cause: " + cause.meta.displayName + " (" + cause.name + " v" + cause.meta.version + ")\n")
+ (mods == null ? "<no mod init>" : "Mods: " + (!mods.list().contains(LoadedMod::shouldBeEnabled) ? "none (vanilla)" : mods.list().select(LoadedMod::shouldBeEnabled).toString(", ", mod -> mod.name + ":" + mod.meta.version)))
+ "\n\n" + error;
}
public static void log(Throwable exception){
try{
Core.settings.getDataDirectory().child("crashes").child("crash_" + System.currentTimeMillis() + ".txt")
.writeString(createReport(exception));
}catch(Throwable ignored){
}
}
public static void handle(Throwable exception, Cons<File> writeListener){
try{
try{
//log to file
Log.err(exception);
}catch(Throwable no){
exception.printStackTrace();
}
//try saving game data
try{
settings.manualSave();
}catch(Throwable ignored){}
//don't create crash logs for custom builds, as it's expected
if(OS.username.equals("anuke") && !"steam".equals(Version.modifier)){
System.exit(1);
}
//attempt to load version regardless
if(Version.number == 0){
try{
ObjectMap<String, String> map = new ObjectMap<>();
PropertiesUtils.load(map, new InputStreamReader(CrashHandler.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(createReport(exception));
writeListener.get(file);
}catch(Throwable e){
Log.err("Failed to save local crash report.", e);
}
//attempt to close connections, if applicable
try{
net.dispose();
}catch(Throwable ignored){
}
}catch(Throwable death){
death.printStackTrace();
}
System.exit(1);
}
/** @return the mod that is likely to have caused the supplied crash */
public static @Nullable LoadedMod getModCause(Throwable e){
if(Vars.mods == null) return null;
try{
for(var element : e.getStackTrace()){
String name = element.getClassName();
if(!name.matches("(mindustry|arc|java|javax|sun|jdk)\\..*")){
for(var mod : mods.list()){
if(mod.meta.main != null && getMatches(mod.meta.main, name) > 0){
return mod;
}else if(element.getFileName() != null && element.getFileName().endsWith(".js") && element.getFileName().startsWith(mod.name + "/")){
return mod;
}
}
}
}
}catch(Throwable ignored){}
return null;
}
private static int getMatches(String name1, String name2){
String[] arr1 = name1.split("\\."), arr2 = name2.split("\\.");
int matches = 0;
for(int i = 0; i < Math.min(arr1.length, arr2.length); i++){
if(!arr1[i].equals(arr2[i])){
return i;
}else if(!arr1[i].matches("net|org|com|io")){ //ignore common domain prefixes, as that's usually not enough to call something a "match"
matches ++;
}
}
return matches;
}
private static String writeException(Throwable e){
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
}

View File

@@ -1,197 +0,0 @@
package mindustry.net;
import arc.*;
import arc.Net.*;
import arc.files.*;
import arc.func.*;
import arc.struct.*;
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 mindustry.gen.*;
import java.io.*;
import java.text.*;
import java.util.*;
import static arc.Core.*;
import static mindustry.Vars.net;
import static mindustry.Vars.*;
public class CrashSender{
public static String createReport(String error){
String report = "Mindustry has crashed. How unfortunate.\n";
if(mods.list().size == 0 && Version.build != -1){
report += "Report this at " + Vars.reportIssueURL + "\n\n";
}
return report + "Version: " + Version.combined() + (Vars.headless ? " (Server)" : "") + "\n"
+ "OS: " + System.getProperty("os.name") + " x" + (OS.is64Bit ? "64" : "32") + "\n"
+ "Java Version: " + System.getProperty("java.version") + "\n"
+ "Java Architecture: " + System.getProperty("sun.arch.data.model") + "\n"
+ mods.list().size + " Mods" + (mods.list().isEmpty() ? "" : ": " + mods.list().toString(", ", mod -> mod.name + ":" + mod.meta.version))
+ "\n\n" + error;
}
public static void log(Throwable exception){
try{
Core.settings.getDataDirectory().child("crashes").child("crash_" + System.currentTimeMillis() + ".txt")
.writeString(createReport(Strings.neatError(exception)));
}catch(Throwable ignored){
}
}
public static void send(Throwable exception, Cons<File> writeListener){
try{
try{
//log to file
Log.err(exception);
}catch(Throwable no){
exception.printStackTrace();
}
//try saving game data
try{
settings.manualSave();
}catch(Throwable ignored){}
//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))){
ret();
}
//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(createReport(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)){
ret();
}
}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()){
ret();
}
}catch(Throwable ignored){
}
//do not send exceptions that occur for versions that can't be parsed
if(Version.number == 0){
ret();
}
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(Groups.player.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, exit code indicates send success
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();
}
ret();
}
private static void ret(){
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

@@ -3,9 +3,10 @@ package mindustry.net;
import arc.*;
import arc.func.*;
import arc.net.*;
import arc.net.Server.*;
import arc.struct.*;
import arc.util.*;
import arc.util.pooling.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.net.Packets.*;
import mindustry.net.Streamable.*;
@@ -14,24 +15,60 @@ import net.jpountz.lz4.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.concurrent.*;
import static arc.util.Log.*;
import static mindustry.Vars.*;
@SuppressWarnings("unchecked")
public class Net{
private static Seq<Prov<? extends Packet>> packetProvs = new Seq<>();
private static Seq<Class<? extends Packet>> packetClasses = new Seq<>();
private static ObjectIntMap<Class<?>> packetToId = new ObjectIntMap<>();
private boolean server;
private boolean active;
private boolean clientLoaded;
private @Nullable StreamBuilder currentStream;
private final Seq<Object> packetQueue = new Seq<>();
private final Seq<Packet> packetQueue = new Seq<>();
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 ExecutorService pingExecutor =
OS.isWindows && !OS.is64Bit ? Threads.boundedExecutor("Ping Servers", 5) : //on 32-bit windows, thread spam crashes
OS.isIos ? Threads.boundedExecutor("Ping Servers", 32) : //on IOS, 256 threads can crash, so limit the amount
Threads.unboundedExecutor();
private final NetProvider provider;
private final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor();
private final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor();
static{
registerPacket(StreamBegin::new);
registerPacket(StreamChunk::new);
registerPacket(WorldStream::new);
registerPacket(ConnectPacket::new);
//register generated packet classes
Call.registerPackets();
}
/** Registers a new packet type for serialization. */
public static <T extends Packet> void registerPacket(Prov<T> cons){
packetProvs.add(cons);
var t = cons.get();
packetClasses.add(t.getClass());
packetToId.put(t.getClass(), packetProvs.size - 1);
}
public static byte getPacketId(Packet packet){
int id = packetToId.get(packet.getClass(), -1);
if(id == -1) throw new ArcRuntimeException("Unknown packet type: " + packet.getClass());
return (byte)id;
}
public static <T extends Packet> T newPacket(byte id){
return ((Prov<T>)packetProvs.get(id & 0xff)).get();
}
public Net(NetProvider provider){
this.provider = provider;
@@ -39,9 +76,9 @@ public class Net{
public void handleException(Throwable e){
if(e instanceof ArcNetException){
Core.app.post(() -> showError(new IOException("mismatch")));
Core.app.post(() -> showError(new IOException("mismatch", e)));
}else if(e instanceof ClosedChannelException){
Core.app.post(() -> showError(new IOException("alreadyconnected")));
Core.app.post(() -> showError(new IOException("alreadyconnected", e)));
}else{
Core.app.post(() -> showError(e));
}
@@ -63,9 +100,9 @@ public class Net{
String type = t.getClass().toString().toLowerCase();
boolean isError = false;
if(e instanceof BufferUnderflowException || e instanceof BufferOverflowException){
if(e instanceof BufferUnderflowException || e instanceof BufferOverflowException || e.getCause() instanceof EOFException){
error = Core.bundle.get("error.io");
}else if(error.equals("mismatch")){
}else if(error.equals("mismatch") || e instanceof LZ4Exception || (e instanceof IndexOutOfBoundsException && e.getStackTrace().length > 0 && e.getStackTrace()[0].getClassName().contains("java.nio"))){
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");
@@ -122,6 +159,7 @@ public class Net{
public void connect(String ip, int port, Runnable success){
try{
if(!active){
Events.fire(new ClientServerConnectEvent(ip, port));
provider.connectClient(ip, port, success);
active = true;
server = false;
@@ -171,14 +209,6 @@ public class Net{
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.
@@ -195,21 +225,21 @@ public class Net{
}
/** Send an object to all connected clients, or to the server if this is a client.*/
public void send(Object object, SendMode mode){
public void send(Object object, boolean reliable){
if(server){
for(NetConnection con : provider.getConnections()){
con.send(object, mode);
con.send(object, reliable);
}
}else{
provider.sendClient(object, mode);
provider.sendClient(object, reliable);
}
}
/** Send an object to everyone EXCEPT a certain client. Server-side only.*/
public void sendExcept(NetConnection except, Object object, SendMode mode){
public void sendExcept(NetConnection except, Object object, boolean reliable){
for(NetConnection con : getConnections()){
if(con != except){
con.send(object, mode);
con.send(object, reliable);
}
}
}
@@ -235,7 +265,8 @@ public class Net{
/**
* Call to handle a packet being received for the client.
*/
public void handleClientReceived(Object object){
public void handleClientReceived(Packet object){
object.handled();
if(object instanceof StreamBegin b){
streams.put(b.id, currentStream = new StreamBuilder(b));
@@ -246,48 +277,76 @@ public class Net{
throw new RuntimeException("Received stream chunk without a StreamBegin beforehand!");
}
builder.add(c.data);
ui.loadfrag.setProgress(builder.progress());
ui.loadfrag.snapProgress();
netClient.resetTimeout();
if(builder.isDone()){
streams.remove(builder.id);
handleClientReceived(builder.build());
currentStream = null;
}
}else if(clientListeners.get(object.getClass()) != null){
}else{
int p = object.getPriority();
if(clientLoaded || ((object instanceof Packet) && ((Packet)object).isImportant())){
if(clientLoaded || p == Packet.priorityHigh){
if(clientListeners.get(object.getClass()) != null){
clientListeners.get(object.getClass()).get(object);
}else{
object.handleClient();
}
Pools.free(object);
}else if(!((object instanceof Packet) && ((Packet)object).isUnimportant())){
}else if(p != Packet.priorityLow){
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){
public void handleServerReceived(NetConnection connection, Packet object){
if(serverListeners.get(object.getClass()) != null){
if(serverListeners.get(object.getClass()) != null){
serverListeners.get(object.getClass()).get(connection, object);
try{
if(connection.hasConnected || object.getPriority() == Packet.priorityHigh){
object.handled();
//handle object normally
if(serverListeners.get(object.getClass()) != null){
serverListeners.get(object.getClass()).get(connection, object);
}else{
object.handleServer(connection);
}
}
}catch(ValidateException e){
//ignore invalid actions
debug("Validation failed for '@': @", e.player, e.getMessage());
}catch(RuntimeException e){
//ignore indirect ValidateException-s
if(e.getCause() instanceof ValidateException v){
debug("Validation failed for '@': @", v.player, v.getMessage());
}else{
//rethrow if not ValidateException
throw e;
}
Pools.free(object);
}else{
Log.err("Unhandled packet type: '@'!", object.getClass());
}
}
/** Sets a connection filter by IP address. If the filter returns {@code false}, the connection will be closed. Server only. */
public void setConnectFilter(@Nullable ServerConnectFilter filter){
provider.setConnectFilter(filter);
}
public @Nullable ServerConnectFilter getConnectFilter(){
return provider.getConnectFilter();
}
/**
* Pings a host in an new thread. If an error occured, failed() should be called with the exception.
* Pings a host in a pooled thread. If an error occurred, failed() should be called with the exception.
* If the port is the default mindustry port, SRV records are checked too.
*/
public void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed){
provider.pingHost(address, port, valid, failed);
pingExecutor.submit(() -> provider.pingHost(address, port, valid, failed));
}
/**
@@ -317,17 +376,13 @@ public class Net{
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);
void sendClient(Object object, boolean reliable);
/** Disconnect from the server. */
void disconnectClient();
@@ -339,7 +394,10 @@ public class Net{
*/
void discoverServers(Cons<Host> callback, Runnable done);
/** Ping a host. If an error occured, failed() should be called with the exception. */
/**
* Ping a host. If an error occurred, failed() should be called with the exception. This method should block.
* If the port is the default mindustry port (6567), SRV records are checked too.
*/
void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed);
/** Host a server at specified port. */
@@ -356,5 +414,12 @@ public class Net{
disconnectClient();
closeServer();
}
/** Sets a connection filter by IP address. If the filter returns {@code false}, the connection will be closed. */
default void setConnectFilter(Server.ServerConnectFilter connectFilter){}
default @Nullable ServerConnectFilter getConnectFilter(){
return null;
}
}
}

View File

@@ -4,8 +4,6 @@ import arc.struct.*;
import arc.util.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.net.Administration.*;
import mindustry.net.Net.*;
import mindustry.net.Packets.*;
import java.io.*;
@@ -18,13 +16,22 @@ public abstract class NetConnection{
public boolean mobile, modclient;
public @Nullable Player player;
public boolean kicked = false;
public long syncTime;
/** When this connection was established. */
public long connectTime = Time.millis();
/** ID of last received client snapshot. */
public int lastReceivedClientSnapshot = -1;
/** Count of snapshots sent from server. */
public int snapshotsSent;
/** Timestamp of last received snapshot. */
public long lastReceivedClientTime;
/** Build requests that have been recently rejected. This is cleared every snapshot. */
public Seq<BuildPlan> rejectedRequests = new Seq<>();
/** Handles chat spam rate limits. */
public Ratekeeper chatRate = new Ratekeeper();
/** Handles packet spam rate limits. */
public Ratekeeper packetRate = new Ratekeeper();
public boolean hasConnected, hasBegunConnecting, hasDisconnected;
public float viewWidth, viewHeight, viewX, viewY;
@@ -33,47 +40,57 @@ public abstract class NetConnection{
this.address = address;
}
/** Kick with the standard kick reason. */
public void kick(){
kick(KickReason.kick);
}
/** Kick with a special, localized reason. Use this if possible. */
public void kick(KickReason reason){
if(kicked) return;
kick(reason, (reason == KickReason.kick || reason == KickReason.banned || reason == KickReason.vote) ? 30 * 1000 : 0);
}
Log.info("Kicking connection @; Reason: @", address, reason.name());
if((reason == KickReason.kick || reason == KickReason.banned || reason == KickReason.vote)){
PlayerInfo info = netServer.admins.getInfo(uuid);
info.timesKicked++;
info.lastKicked = Math.max(Time.millis() + 30 * 1000, info.lastKicked);
}
Call.kick(this, reason);
Time.runTask(2f, this::close);
netServer.admins.save();
kicked = true;
/** Kick with a special, localized reason. Use this if possible. */
public void kick(KickReason reason, long kickDuration){
kick(null, reason, kickDuration);
}
/** Kick with an arbitrary reason. */
public void kick(String reason){
kick(reason, 30 * 1000);
kick(reason, null, 30 * 1000);
}
/** Kick with an arbitrary reason. */
public void kick(String reason, long duration){
kick(reason, null, duration);
}
/** Kick with an arbitrary reason, and a kick duration in milliseconds. */
public void kick(String reason, int kickDuration){
private void kick(String reason, @Nullable KickReason kickType, long kickDuration){
if(kicked) return;
Log.info("Kicking connection @; Reason: @", address, reason.replace("\n", " "));
Log.info("Kicking connection @ / @; Reason: @", address, uuid, reason == null ? kickType.name() : reason.replace("\n", " "));
netServer.admins.handleKicked(uuid, address, kickDuration);
if(kickDuration > 0){
netServer.admins.handleKicked(uuid, address, kickDuration);
}
Call.kick(this, reason);
if(reason == null){
Call.kick(this, kickType);
}else{
Call.kick(this, reason);
}
Time.runTask(2f, this::close);
kickDisconnect();
netServer.admins.save();
kicked = true;
}
protected void kickDisconnect(){
close();
}
public boolean isConnected(){
return true;
}
@@ -83,25 +100,25 @@ public abstract class NetConnection{
int cid;
StreamBegin begin = new StreamBegin();
begin.total = stream.stream.available();
begin.type = Registrator.getID(stream.getClass());
send(begin, SendMode.tcp);
begin.type = Net.getPacketId(stream);
send(begin, true);
cid = begin.id;
while(stream.stream.available() > 0){
byte[] bytes = new byte[Math.min(512, stream.stream.available())];
byte[] bytes = new byte[Math.min(maxTcpSize, stream.stream.available())];
stream.stream.read(bytes);
StreamChunk chunk = new StreamChunk();
chunk.id = cid;
chunk.data = bytes;
send(chunk, SendMode.tcp);
send(chunk, true);
}
}catch(IOException e){
throw new RuntimeException(e);
}
}
public abstract void send(Object object, SendMode mode);
public abstract void send(Object object, boolean reliable);
public abstract void close();
}

View File

@@ -10,8 +10,10 @@ import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.io.*;
import mindustry.logic.*;
import mindustry.maps.Map;
import mindustry.net.Administration.*;
import mindustry.type.*;
import java.io.*;
import java.nio.*;
@@ -29,24 +31,31 @@ public class NetworkIO{
state.rules.researched.clear();
for(ContentType type : ContentType.all){
for(Content c : content.getBy(type)){
if(c instanceof UnlockableContent u && u.unlocked() && TechTree.get(u) != null){
state.rules.researched.add(u.name);
if(c instanceof UnlockableContent u && u.unlocked() && u.techNode != null){
state.rules.researched.add(u);
}
}
}
}
stream.writeUTF(JsonIO.write(state.rules));
stream.writeUTF(JsonIO.write(state.mapLocales));
SaveIO.getSaveWriter().writeStringMap(stream, state.map.tags);
stream.writeInt(state.wave);
stream.writeFloat(state.wavetime);
stream.writeDouble(state.tick);
stream.writeLong(GlobalVars.rand.seed0);
stream.writeLong(GlobalVars.rand.seed1);
stream.writeInt(player.id);
player.write(Writes.get(stream));
player.write(new Writes(stream));
SaveIO.getSaveWriter().writeContentHeader(stream);
SaveIO.getSaveWriter().writeMap(stream);
SaveIO.getSaveWriter().writeTeamBlocks(stream);
SaveIO.getSaveWriter().writeMarkers(stream);
SaveIO.getSaveWriter().writeCustomChunks(stream, true);
}catch(IOException e){
throw new RuntimeException(e);
}
@@ -57,20 +66,29 @@ public class NetworkIO{
try(DataInputStream stream = new DataInputStream(is)){
Time.clear();
state.rules = JsonIO.read(Rules.class, stream.readUTF());
state.mapLocales = JsonIO.read(MapLocales.class, stream.readUTF());
state.map = new Map(SaveIO.getSaveWriter().readStringMap(stream));
state.wave = stream.readInt();
state.wavetime = stream.readFloat();
state.tick = stream.readDouble();
GlobalVars.rand.seed0 = stream.readLong();
GlobalVars.rand.seed1 = stream.readLong();
Reads read = new Reads(stream);
Groups.clear();
int id = stream.readInt();
player.reset();
player.read(Reads.get(stream));
player.read(read);
player.id = id;
player.add();
SaveIO.getSaveWriter().readContentHeader(stream);
SaveIO.getSaveWriter().readMap(stream, world.context);
SaveIO.getSaveWriter().readTeamBlocks(stream);
SaveIO.getSaveWriter().readMarkers(stream);
SaveIO.getSaveWriter().readCustomChunks(stream);
}catch(IOException e){
throw new RuntimeException(e);
}finally{
@@ -79,7 +97,7 @@ public class NetworkIO{
}
public static ByteBuffer writeServerData(){
String name = (headless ? Config.name.string() : player.name);
String name = (headless ? Config.serverName.string() : player.name);
String description = headless && !Config.desc.string().equals("off") ? Config.desc.string() : "";
String map = state.map.name();

View File

@@ -1,19 +1,37 @@
package mindustry.net;
import arc.util.pooling.Pool.*;
import arc.util.io.*;
import java.nio.*;
import java.io.*;
public interface Packet extends Poolable{
default void read(ByteBuffer buffer){}
default void write(ByteBuffer buffer){}
default void reset(){}
public abstract class Packet{
//internally used by generated code
protected static final byte[] NODATA = {};
protected static final ReusableByteInStream BAIS = new ReusableByteInStream();
protected static final Reads READ = new Reads(new DataInputStream(BAIS));
default boolean isImportant(){
return false;
//these are constants because I don't want to bother making an enum to mirror the annotation enum
/** Does not get handled unless client is connected. */
public static final int priorityLow = 0;
/** Gets put in a queue and processed if not connected. */
public static final int priorityNormal = 1;
/** Gets handled immediately, regardless of connection status. */
public static final int priorityHigh = 2;
public void read(Reads read){}
public void write(Writes write){}
public void read(Reads read, int length){
read(read);
}
default boolean isUnimportant(){
return false;
public void handled(){}
public int getPriority(){
return priorityNormal;
}
public void handleClient(){}
public void handleServer(NetConnection con){}
}

View File

@@ -7,13 +7,9 @@ import arc.util.serialization.*;
import mindustry.core.*;
import mindustry.io.*;
import java.io.*;
import java.nio.*;
import java.util.zip.*;
/**
* Class for storing all packets.
*/
/** Class for storing all packets. */
public class Packets{
public enum KickReason{
@@ -21,6 +17,8 @@ public class Packets{
nameInUse, idInUse, nameEmpty, customClient, serverClose, vote, typeMismatch,
whitelist, playerLimit, serverRestarting;
public static final KickReason[] all = values();
public final boolean quiet;
KickReason(){
@@ -42,24 +40,28 @@ public class Packets{
}
public enum AdminAction{
kick, ban, trace, wave
kick, ban, trace, wave, switchTeam;
public static final AdminAction[] all = values();
}
public static class Connect implements Packet{
/** Generic client connection event. */
public static class Connect extends Packet{
public String addressTCP;
@Override
public boolean isImportant(){
return true;
public int getPriority(){
return priorityHigh;
}
}
public static class Disconnect implements Packet{
/** Generic client disconnection event. */
public static class Disconnect extends Packet{
public String reason;
@Override
public boolean isImportant(){
return true;
public int getPriority(){
return priorityHigh;
}
}
@@ -67,55 +69,8 @@ public class Packets{
}
public static class InvokePacket implements Packet{
private static ReusableByteInStream bin;
private static Reads read = new Reads(new DataInputStream(bin = new ReusableByteInStream()));
public byte type, priority;
public byte[] bytes;
public int length;
@Override
public void read(ByteBuffer buffer){
type = buffer.get();
priority = buffer.get();
short writeLength = buffer.getShort();
bytes = new byte[writeLength];
buffer.get(bytes);
}
@Override
public void write(ByteBuffer buffer){
buffer.put(type);
buffer.put(priority);
buffer.putShort((short)length);
buffer.put(bytes, 0, length);
}
@Override
public void reset(){
priority = 0;
}
@Override
public boolean isImportant(){
return priority == 1;
}
@Override
public boolean isUnimportant(){
return priority == 2;
}
public Reads reader(){
bin.setBytes(bytes);
return read;
}
}
/** Marks the beginning of a stream. */
public static class StreamBegin implements Packet{
public static class StreamBegin extends Packet{
private static int lastid;
public int id = lastid++;
@@ -123,84 +78,89 @@ public class Packets{
public byte type;
@Override
public void write(ByteBuffer buffer){
buffer.putInt(id);
buffer.putInt(total);
buffer.put(type);
public void write(Writes buffer){
buffer.i(id);
buffer.i(total);
buffer.b(type);
}
@Override
public void read(ByteBuffer buffer){
id = buffer.getInt();
total = buffer.getInt();
type = buffer.get();
public void read(Reads buffer){
id = buffer.i();
total = buffer.i();
type = buffer.b();
}
}
public static class StreamChunk implements Packet{
public static class StreamChunk extends Packet{
public int id;
public byte[] data;
@Override
public void write(ByteBuffer buffer){
buffer.putInt(id);
buffer.putShort((short)data.length);
buffer.put(data);
public void write(Writes buffer){
buffer.i(id);
buffer.s((short)data.length);
buffer.b(data);
}
@Override
public void read(ByteBuffer buffer){
id = buffer.getInt();
data = new byte[buffer.getShort()];
buffer.get(data);
public void read(Reads buffer){
id = buffer.i();
data = buffer.b(buffer.s());
}
}
public static class ConnectPacket implements Packet{
public static class ConnectPacket extends Packet{
public int version;
public String versionType;
public Seq<String> mods;
public String name, uuid, usid;
public String name, locale, uuid, usid;
public boolean mobile;
public int color;
@Override
public void write(ByteBuffer buffer){
buffer.putInt(Version.build);
public void write(Writes buffer){
buffer.i(Version.build);
TypeIO.writeString(buffer, versionType);
TypeIO.writeString(buffer, name);
TypeIO.writeString(buffer, locale);
TypeIO.writeString(buffer, usid);
byte[] b = Base64Coder.decode(uuid);
buffer.put(b);
buffer.b(b);
CRC32 crc = new CRC32();
crc.update(Base64Coder.decode(uuid), 0, b.length);
buffer.putLong(crc.getValue());
buffer.l(crc.getValue());
buffer.put(mobile ? (byte)1 : 0);
buffer.putInt(color);
buffer.put((byte)mods.size);
buffer.b(mobile ? (byte)1 : 0);
buffer.i(color);
buffer.b((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();
public void read(Reads buffer){
version = buffer.i();
versionType = TypeIO.readString(buffer);
name = TypeIO.readString(buffer);
locale = TypeIO.readString(buffer);
usid = TypeIO.readString(buffer);
byte[] idbytes = new byte[16];
buffer.get(idbytes);
byte[] idbytes = buffer.b(16);
uuid = new String(Base64Coder.encode(idbytes));
mobile = buffer.get() == 1;
color = buffer.getInt();
int totalMods = buffer.get();
mobile = buffer.b() == 1;
color = buffer.i();
int totalMods = buffer.b();
mods = new Seq<>(totalMods);
for(int i = 0; i < totalMods; i++){
mods.add(TypeIO.readString(buffer));
}
}
@Override
public int getPriority(){
return priorityHigh;
}
}
}

View File

@@ -1,45 +0,0 @@
package mindustry.net;
import arc.func.*;
import arc.struct.*;
import mindustry.net.Packets.*;
public class Registrator{
private static final 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 final 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

@@ -5,10 +5,16 @@ import arc.*;
public class ServerGroup{
public String name;
public String[] addresses;
public boolean prioritized = false;
public ServerGroup(String name, String[] addresses){
public ServerGroup(String name, String[] addresses, boolean prioritized){
this.name = name;
this.addresses = addresses;
this.prioritized = prioritized;
}
public ServerGroup(String name, String[] addresses){
this(name, addresses, false);
}
public ServerGroup(){

View File

@@ -4,12 +4,12 @@ import mindustry.net.Packets.*;
import java.io.*;
public class Streamable implements Packet{
public class Streamable extends Packet{
public transient ByteArrayInputStream stream;
@Override
public boolean isImportant(){
return true;
public int getPriority(){
return priorityHigh;
}
public static class StreamBuilder{
@@ -37,7 +37,7 @@ public class Streamable implements Packet{
}
public Streamable build(){
Streamable s = (Streamable)Registrator.getByID(type).constructor.get();
Streamable s = Net.newPacket(type);
s.stream = new ByteArrayInputStream(stream.toByteArray());
return s;
}

View File

@@ -32,7 +32,9 @@ public class WorldReloader{
Call.worldDataBegin();
}else{
net.reset();
if(net.client()){
net.reset();
}
logic.reset();
}