2828import com .google .gson .JsonArray ;
2929import com .google .gson .JsonObject ;
3030import com .google .gson .JsonSyntaxException ;
31+ import java .net .ConnectException ;
32+ import java .net .UnknownHostException ;
33+ import java .nio .charset .StandardCharsets ;
34+ import java .util .Deque ;
35+ import java .util .Iterator ;
36+ import java .util .LinkedList ;
37+ import java .util .List ;
38+ import java .util .concurrent .ThreadLocalRandom ;
39+ import java .util .concurrent .TimeUnit ;
40+ import javax .net .ssl .SSLException ;
3141import lombok .Getter ;
42+ import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
3243import org .geysermc .floodgate .pluginmessage .PluginMessageChannels ;
3344import org .geysermc .floodgate .util .WebsocketEventType ;
3445import org .geysermc .geyser .Constants ;
4051import org .java_websocket .client .WebSocketClient ;
4152import org .java_websocket .handshake .ServerHandshake ;
4253
43- import javax .net .ssl .SSLException ;
44- import java .net .ConnectException ;
45- import java .net .UnknownHostException ;
46- import java .nio .charset .StandardCharsets ;
47- import java .util .ArrayList ;
48- import java .util .Iterator ;
49- import java .util .List ;
50- import java .util .concurrent .ThreadLocalRandom ;
51- import java .util .concurrent .TimeUnit ;
52-
5354public final class FloodgateSkinUploader {
54- private final List < String > skinQueue = new ArrayList <>() ;
55+ private static final int MAX_QUEUED_ENTRIES = 500 ;
5556
5657 private final GeyserLogger logger ;
5758 private final WebSocketClient client ;
5859 private volatile boolean closed ;
5960
61+ /**
62+ * Queue skins in case the global api is temporarily unavailable, so that players will have their skin when the
63+ * global api comes back online.
64+ * However, we only start queueing skins if the websocket has been opened before, which is why this is nullable.
65+ * Some servers block access to external sites such as our global api, in which case the skin upload queue would
66+ * grow without the skins having a chance to be actually uploaded.
67+ * We'll lose player skins if the server started while the global api was offline, but that's worth the trade-off.
68+ */
69+ private @ MonotonicNonNull Deque <String > skinQueue = null ;
70+
6071 @ Getter private int id ;
6172 @ Getter private String verifyCode ;
6273 @ Getter private int subscribersCount ;
@@ -68,10 +79,19 @@ public FloodgateSkinUploader(GeyserImpl geyser) {
6879 public void onOpen (ServerHandshake handshake ) {
6980 setConnectionLostTimeout (11 );
7081
71- Iterator <String > queueIterator = skinQueue .iterator ();
72- while (isOpen () && queueIterator .hasNext ()) {
73- send (queueIterator .next ());
74- queueIterator .remove ();
82+ boolean hasSkinQueue = skinQueue != null ;
83+
84+ if (!hasSkinQueue ) {
85+ skinQueue = new LinkedList <>();
86+ return ;
87+ }
88+
89+ synchronized (skinQueue ) {
90+ Iterator <String > queueIterator = skinQueue .iterator ();
91+ while (isOpen () && queueIterator .hasNext ()) {
92+ send (queueIterator .next ());
93+ queueIterator .remove ();
94+ }
7595 }
7696 }
7797
@@ -210,7 +230,16 @@ public void uploadSkin(GeyserSession session) {
210230 client .send (jsonString );
211231 return ;
212232 }
213- skinQueue .add (jsonString );
233+
234+ if (skinQueue != null ) {
235+ synchronized (skinQueue ) {
236+ // Only keep the most recent skins if we hit the limit, as it's more likely that they're still online
237+ if (skinQueue .size () >= MAX_QUEUED_ENTRIES ) {
238+ skinQueue .removeFirst ();
239+ }
240+ skinQueue .addLast (jsonString );
241+ }
242+ }
214243 }
215244
216245 private void reconnectLater (GeyserImpl geyser ) {
0 commit comments