Merge branch 'master' into port-field
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
154
core/src/mindustry/net/CrashHandler.java
Normal file
154
core/src/mindustry/net/CrashHandler.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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){
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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){}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ public class WorldReloader{
|
||||
|
||||
Call.worldDataBegin();
|
||||
}else{
|
||||
net.reset();
|
||||
if(net.client()){
|
||||
net.reset();
|
||||
}
|
||||
logic.reset();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user