From e58782c838fcff460348f0c1c7efdc36f1abf309 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 4 Jul 2026 13:01:24 -0400 Subject: [PATCH] Bulk-serialization optimization --- core/src/mindustry/core/NetServer.java | 28 +++++++++---- core/src/mindustry/net/ArcNetProvider.java | 49 ++++++++++++++++++++-- core/src/mindustry/net/Net.java | 39 +++++++++++++---- gradle.properties | 2 +- 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index 29973972e9..33712b3482 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -131,6 +131,7 @@ public class NetServer implements ApplicationListener{ private ObjectMap>> logicClientDataHandlers = new ObjectMap<>(); /** Reused Seq for writing entity snapshots per team. */ private Seq playersToSend = new Seq<>(false); + private Seq tempConnections = new Seq<>(false); /** Used for entity snapshot timing. */ public long snapshotSyncTime; @@ -1145,6 +1146,13 @@ public class NetServer implements ApplicationListener{ hiddenIds.clear(); int sent = 0; + tempConnections.clear(); + + for(Player player : players){ + //player.con must not be null here (the players seq must ONLY contain non-local connected clients) + tempConnections.add(player.con); + player.con.snapshotsSent ++; + } for(Syncc entity : Groups.sync){ if(entity.isSyncHidden(team)){ @@ -1158,9 +1166,7 @@ public class NetServer implements ApplicationListener{ if(syncStream.size() > maxSnapshotSize){ dataStream.close(); - short ssent = (short)sent; - var bytes = syncStream.toByteArray(); - players.each(player -> Call.entitySnapshot(player.con, ssent, bytes)); + sendEntitySnapshots(tempConnections, (short)sent, syncStream.toByteArray()); sent = 0; syncStream.reset(); } @@ -1168,18 +1174,22 @@ public class NetServer implements ApplicationListener{ if(sent > 0){ dataStream.close(); - short ssent = (short)sent; - var bytes = syncStream.toByteArray(); - players.each(player -> Call.entitySnapshot(player.con, ssent, bytes)); + sendEntitySnapshots(tempConnections, (short)sent, syncStream.toByteArray()); } if(hiddenIds.size > 0){ - players.each(player -> Call.hiddenSnapshot(player.con, hiddenIds)); + var packet = new HiddenSnapshotCallPacket(); + packet.ids = hiddenIds; + net.send(packet, tempConnections, false); } - - players.each(player -> player.con.snapshotsSent++); } + protected void sendEntitySnapshots(Seq connections, short amount, byte[] data){ + var packet = new EntitySnapshotCallPacket(); + packet.amount = amount; + packet.data = data; + net.send(packet, connections, false); + } /** Writes a custom snapshot containing player-local entities; this is for entities other players don't see. */ public void writeCustomEntitySnapshot(Player player, Iterable entities) throws IOException{ diff --git a/core/src/mindustry/net/ArcNetProvider.java b/core/src/mindustry/net/ArcNetProvider.java index c1e753bc7e..b0b88d7d41 100644 --- a/core/src/mindustry/net/ArcNetProvider.java +++ b/core/src/mindustry/net/ArcNetProvider.java @@ -36,6 +36,7 @@ public class ArcNetProvider implements NetProvider{ private static final LZ4SafeDecompressor decompressor = LZ4Factory.fastestInstance().safeDecompressor(); private static final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor(); + private static final ThreadLocal> writeConnections = Threads.local(Seq::new); private volatile int playerLimitCache, packetSpamLimit; private Ratekeeper clientUdpErrorRate = new Ratekeeper(); @@ -329,6 +330,49 @@ public class ArcNetProvider implements NetProvider{ return connections; } + @Override + public void sendAllServer(Object object, Iterable connections, boolean reliable){ + //build up list of underlying arcnet connections for faster bulk transfer + var cons = writeConnections.get(); + cons.clear(); + for(var con : connections){ + if(con instanceof ArcConnection ac){ + cons.add(ac.connection); + } + } + + if(reliable){ + server.sendToAllTCP(object, cons); + }else{ + server.sendToAllUDP(object, cons); + } + + cons.clear(); + } + + @Override + public void sendAllServer(Object object, boolean reliable){ + if(reliable){ + server.sendToAllTCP(object); + }else{ + server.sendToAllUDP(object); + } + } + + @Override + public void sendExceptServer(NetConnection except, Object object, boolean reliable){ + if(!(except instanceof ArcConnection con)){ + NetProvider.super.sendExceptServer(except, object, reliable); + return; + } + + if(reliable){ + server.sendToAllExceptTCP(con.connection.getID(), object); + }else{ + server.sendToAllExceptUDP(con.connection.getID(), object); + } + } + @Override public void hostServer(int port) throws IOException{ connections.clear(); @@ -351,7 +395,7 @@ public class ArcNetProvider implements NetProvider{ mainExecutor.submit(server::stop); } - class ArcConnection extends NetConnection{ + public class ArcConnection extends NetConnection{ public final Connection connection; long lastErrorTime; @@ -413,8 +457,7 @@ public class ArcNetProvider implements NetProvider{ } } }catch(Exception e){ - Log.err(e); - Log.info("Error sending packet. Disconnecting invalid client!"); + Log.err("Error sending packet. Disconnecting invalid client!", e); connection.close(DcReason.error); if(connection.getArbitraryData() instanceof ArcConnection k){ diff --git a/core/src/mindustry/net/Net.java b/core/src/mindustry/net/Net.java index 434a10d00d..53aea8b5fd 100644 --- a/core/src/mindustry/net/Net.java +++ b/core/src/mindustry/net/Net.java @@ -243,21 +243,20 @@ public class Net{ /** Send an object to all connected clients, or to the server if this is a client.*/ public void send(Object object, boolean reliable){ if(server){ - for(NetConnection con : provider.getConnections()){ - con.send(object, reliable); - } + provider.sendAllServer(object, reliable); }else{ provider.sendClient(object, reliable); } } + /** Server bulk-send to several clients. */ + public void send(Object object, Iterable connections, boolean reliable){ + provider.sendAllServer(object, connections, reliable); + } + /** Send an object to everyone EXCEPT a certain client. Server-side only.*/ public void sendExcept(NetConnection except, Object object, boolean reliable){ - for(NetConnection con : getConnections()){ - if(con != except){ - con.send(object, reliable); - } - } + provider.sendExceptServer(except, object, reliable); } public @Nullable StreamBuilder getCurrentStream(){ @@ -413,6 +412,30 @@ public class Net{ /** Networking implementation. */ public interface NetProvider{ + + /** Sends a packet to a specific list of clients. */ + default void sendAllServer(Object object, Iterable connections, boolean reliable){ + for(NetConnection con : connections){ + con.send(object, reliable); + } + } + + /** Sends a packet to all connected clients. */ + default void sendAllServer(Object object, boolean reliable){ + for(NetConnection con : getConnections()){ + con.send(object, reliable); + } + } + + /** Sends a packet to all connected clients, except the specified one. */ + default void sendExceptServer(NetConnection except, Object object, boolean reliable){ + for(NetConnection con : getConnections()){ + if(con != except){ + con.send(object, reliable); + } + } + } + /** Connect to a server. */ void connectClient(String ip, int port, Runnable success) throws IOException; diff --git a/gradle.properties b/gradle.properties index eae5e4886d..035ba4a875 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=da9887f6e4 +archash=f229d1fb3f