--- a/net/minecraft/server/players/GameProfileCache.java
+++ b/net/minecraft/server/players/GameProfileCache.java
@@ -1,3 +1,4 @@
+// mc-dev import
 package net.minecraft.server.players;
 
 import com.google.common.collect.ImmutableList;
@@ -15,21 +16,23 @@
 import com.mojang.authlib.GameProfileRepository;
 import com.mojang.authlib.ProfileLookupCallback;
 import com.mojang.logging.LogUtils;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.Reader;
-import java.io.Writer;
 import java.nio.charset.StandardCharsets;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
@@ -44,6 +47,7 @@
 import org.slf4j.Logger;
 
 public class GameProfileCache {
+
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final int GAMEPROFILES_MRU_LIMIT = 1000;
     private static final int GAMEPROFILES_EXPIRATION_MONTHS = 1;
@@ -58,6 +62,11 @@
     @Nullable
     private Executor executor;
 
+    // Paper start
+    protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock();
+    protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock();
+    // Paper end
+
     public GameProfileCache(GameProfileRepository profileRepository, File cacheFile) {
         this.profileRepository = profileRepository;
         this.file = cacheFile;
@@ -65,109 +74,143 @@
     }
 
     private void safeAdd(GameProfileCache.GameProfileInfo entry) {
-        GameProfile gameProfile = entry.getProfile();
+        try { this.stateLock.lock(); // Paper - allow better concurrency
+        GameProfile gameprofile = entry.getProfile();
+
         entry.setLastAccess(this.getNextOperation());
-        String string = gameProfile.getName();
-        if (string != null) {
-            this.profilesByName.put(string.toLowerCase(Locale.ROOT), entry);
+        String s = gameprofile.getName();
+
+        if (s != null) {
+            this.profilesByName.put(s.toLowerCase(Locale.ROOT), entry);
         }
 
-        UUID uUID = gameProfile.getId();
-        if (uUID != null) {
-            this.profilesByUUID.put(uUID, entry);
+        UUID uuid = gameprofile.getId();
+
+        if (uuid != null) {
+            this.profilesByUUID.put(uuid, entry);
         }
+        } finally { this.stateLock.unlock(); } // Paper - allow better concurrency
 
     }
 
     private static Optional<GameProfile> lookupGameProfile(GameProfileRepository repository, String name) {
-        final AtomicReference<GameProfile> atomicReference = new AtomicReference<>();
-        ProfileLookupCallback profileLookupCallback = new ProfileLookupCallback() {
-            public void onProfileLookupSucceeded(GameProfile gameProfile) {
-                atomicReference.set(gameProfile);
+        final AtomicReference<GameProfile> atomicreference = new AtomicReference();
+        ProfileLookupCallback profilelookupcallback = new ProfileLookupCallback() {
+            public void onProfileLookupSucceeded(GameProfile gameprofile) {
+                atomicreference.set(gameprofile);
             }
 
-            public void onProfileLookupFailed(GameProfile gameProfile, Exception exception) {
-                atomicReference.set((GameProfile)null);
+            public void onProfileLookupFailed(GameProfile gameprofile, Exception exception) {
+                atomicreference.set(null); // CraftBukkit - decompile error
             }
         };
-        repository.findProfilesByNames(new String[]{name}, Agent.MINECRAFT, profileLookupCallback);
-        GameProfile gameProfile = atomicReference.get();
-        if (!usesAuthentication() && gameProfile == null) {
-            UUID uUID = UUIDUtil.getOrCreatePlayerUUID(new GameProfile((UUID)null, name));
-            return Optional.of(new GameProfile(uUID, name));
+
+        if (!org.apache.commons.lang3.StringUtils.isBlank(name) // Paper - Don't lookup a profile with a blank name
+                && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) // Paper - only run in online mode - 100 COL
+        repository.findProfilesByNames(new String[]{name}, Agent.MINECRAFT, profilelookupcallback);
+        GameProfile gameprofile = (GameProfile) atomicreference.get();
+
+        if (!GameProfileCache.usesAuthentication() && gameprofile == null) {
+            UUID uuid = UUIDUtil.getOrCreatePlayerUUID(new GameProfile((UUID) null, name));
+
+            return Optional.of(new GameProfile(uuid, name));
         } else {
-            return Optional.ofNullable(gameProfile);
+            return Optional.ofNullable(gameprofile);
         }
     }
 
     public static void setUsesAuthentication(boolean value) {
-        usesAuthentication = value;
+        GameProfileCache.usesAuthentication = value;
     }
 
     private static boolean usesAuthentication() {
-        return usesAuthentication;
+        return io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode(); // Paper
     }
 
     public void add(GameProfile profile) {
         Calendar calendar = Calendar.getInstance();
+
         calendar.setTime(new Date());
         calendar.add(2, 1);
         Date date = calendar.getTime();
-        GameProfileCache.GameProfileInfo gameProfileInfo = new GameProfileCache.GameProfileInfo(profile, date);
-        this.safeAdd(gameProfileInfo);
-        this.save();
+        GameProfileCache.GameProfileInfo usercache_usercacheentry = new GameProfileCache.GameProfileInfo(profile, date);
+
+        this.safeAdd(usercache_usercacheentry);
+        if( !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly ) this.save(true); // Spigot - skip saving if disabled // Paper - async
     }
 
     private long getNextOperation() {
         return this.operationCount.incrementAndGet();
     }
 
+    // Paper start
+    public @Nullable GameProfile getProfileIfCached(String name) {
+        try { this.stateLock.lock(); // Paper - allow better concurrency
+        GameProfileCache.GameProfileInfo entry = this.profilesByName.get(name.toLowerCase(Locale.ROOT));
+        if (entry == null) {
+            return null;
+        }
+        entry.setLastAccess(this.getNextOperation());
+        return entry.getProfile();
+        } finally { this.stateLock.unlock(); } // Paper - allow better concurrency
+    }
+    // Paper end
+
     public Optional<GameProfile> get(String name) {
-        String string = name.toLowerCase(Locale.ROOT);
-        GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByName.get(string);
-        boolean bl = false;
-        if (gameProfileInfo != null && (new Date()).getTime() >= gameProfileInfo.expirationDate.getTime()) {
-            this.profilesByUUID.remove(gameProfileInfo.getProfile().getId());
-            this.profilesByName.remove(gameProfileInfo.getProfile().getName().toLowerCase(Locale.ROOT));
-            bl = true;
-            gameProfileInfo = null;
+        String s1 = name.toLowerCase(Locale.ROOT);
+        boolean stateLocked = true; try { this.stateLock.lock(); // Paper - allow better concurrency
+        GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1);
+        boolean flag = false;
+
+        if (usercache_usercacheentry != null && (new Date()).getTime() >= usercache_usercacheentry.expirationDate.getTime()) {
+            this.profilesByUUID.remove(usercache_usercacheentry.getProfile().getId());
+            this.profilesByName.remove(usercache_usercacheentry.getProfile().getName().toLowerCase(Locale.ROOT));
+            flag = true;
+            usercache_usercacheentry = null;
         }
 
-        Optional<GameProfile> optional;
-        if (gameProfileInfo != null) {
-            gameProfileInfo.setLastAccess(this.getNextOperation());
-            optional = Optional.of(gameProfileInfo.getProfile());
+        Optional optional;
+
+        if (usercache_usercacheentry != null) {
+            usercache_usercacheentry.setLastAccess(this.getNextOperation());
+            optional = Optional.of(usercache_usercacheentry.getProfile());
+            stateLocked = false; this.stateLock.unlock(); // Paper - allow better concurrency
         } else {
-            optional = lookupGameProfile(this.profileRepository, string);
+            stateLocked = false; this.stateLock.unlock(); // Paper - allow better concurrency
+            try { this.lookupLock.lock(); // Paper - allow better concurrency
+            optional = GameProfileCache.lookupGameProfile(this.profileRepository, name); // Spigot - use correct case for offline players
+            } finally { this.lookupLock.unlock(); } // Paper - allow better concurrency
             if (optional.isPresent()) {
-                this.add(optional.get());
-                bl = false;
+                this.add((GameProfile) optional.get());
+                flag = false;
             }
         }
 
-        if (bl) {
-            this.save();
+        if (flag && !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { // Spigot - skip saving if disabled
+            this.save(true); // Paper
         }
 
         return optional;
+        } finally { if (stateLocked) {  this.stateLock.unlock(); } } // Paper - allow better concurrency
     }
 
     public void getAsync(String username, Consumer<Optional<GameProfile>> consumer) {
         if (this.executor == null) {
             throw new IllegalStateException("No executor");
         } else {
-            CompletableFuture<Optional<GameProfile>> completableFuture = this.requests.get(username);
-            if (completableFuture != null) {
-                this.requests.put(username, completableFuture.whenCompleteAsync((profile, throwable) -> {
-                    consumer.accept(profile);
+            CompletableFuture<Optional<GameProfile>> completablefuture = (CompletableFuture) this.requests.get(username);
+
+            if (completablefuture != null) {
+                this.requests.put(username, completablefuture.whenCompleteAsync((optional, throwable) -> {
+                    consumer.accept(optional);
                 }, this.executor));
             } else {
                 this.requests.put(username, CompletableFuture.supplyAsync(() -> {
                     return this.get(username);
-                }, Util.backgroundExecutor()).whenCompleteAsync((profile, throwable) -> {
+                }, Util.PROFILE_EXECUTOR).whenCompleteAsync((optional, throwable) -> { // Paper - not a good idea to use BLOCKING OPERATIONS on the worldgen executor
                     this.requests.remove(username);
-                }, this.executor).whenCompleteAsync((profile, throwable) -> {
-                    consumer.accept(profile);
+                }, this.executor).whenCompleteAsync((optional, throwable) -> {
+                    consumer.accept(optional);
                 }, this.executor));
             }
 
@@ -175,13 +218,16 @@
     }
 
     public Optional<GameProfile> get(UUID uuid) {
-        GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByUUID.get(uuid);
-        if (gameProfileInfo == null) {
+        try { this.stateLock.lock(); // Paper - allow better concurrency
+        GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByUUID.get(uuid);
+
+        if (usercache_usercacheentry == null) {
             return Optional.empty();
         } else {
-            gameProfileInfo.setLastAccess(this.getNextOperation());
-            return Optional.of(gameProfileInfo.getProfile());
+            usercache_usercacheentry.setLastAccess(this.getNextOperation());
+            return Optional.of(usercache_usercacheentry.getProfile());
         }
+        } finally { this.stateLock.unlock(); } // Paper - allow better concurrency
     }
 
     public void setExecutor(Executor executor) {
@@ -197,128 +243,167 @@
     }
 
     public List<GameProfileCache.GameProfileInfo> load() {
-        List<GameProfileCache.GameProfileInfo> list = Lists.newArrayList();
+        ArrayList arraylist = Lists.newArrayList();
 
         try {
-            Reader reader = Files.newReader(this.file, StandardCharsets.UTF_8);
+            BufferedReader bufferedreader = Files.newReader(this.file, StandardCharsets.UTF_8);
+
+            label54:
+            {
+                ArrayList arraylist1;
 
-            Object var9;
-            label61: {
                 try {
-                    JsonArray jsonArray = this.gson.fromJson(reader, JsonArray.class);
-                    if (jsonArray == null) {
-                        var9 = list;
-                        break label61;
+                    JsonArray jsonarray = (JsonArray) this.gson.fromJson(bufferedreader, JsonArray.class);
+
+                    if (jsonarray != null) {
+                        DateFormat dateformat = GameProfileCache.createDateFormat();
+
+                        jsonarray.forEach((jsonelement) -> {
+                            Optional optional = GameProfileCache.readGameProfile(jsonelement, dateformat);
+
+                            Objects.requireNonNull(arraylist);
+                            optional.ifPresent(arraylist::add);
+                        });
+                        break label54;
                     }
 
-                    DateFormat dateFormat = createDateFormat();
-                    jsonArray.forEach((json) -> {
-                        readGameProfile(json, dateFormat).ifPresent(list::add);
-                    });
-                } catch (Throwable var6) {
-                    if (reader != null) {
+                    arraylist1 = arraylist;
+                } catch (Throwable throwable) {
+                    if (bufferedreader != null) {
                         try {
-                            reader.close();
-                        } catch (Throwable var5) {
-                            var6.addSuppressed(var5);
+                            bufferedreader.close();
+                        } catch (Throwable throwable1) {
+                            throwable.addSuppressed(throwable1);
                         }
                     }
 
-                    throw var6;
+                    throw throwable;
                 }
 
-                if (reader != null) {
-                    reader.close();
+                if (bufferedreader != null) {
+                    bufferedreader.close();
                 }
 
-                return list;
+                return arraylist1;
             }
 
-            if (reader != null) {
-                reader.close();
+            if (bufferedreader != null) {
+                bufferedreader.close();
             }
-
-            return (List<GameProfileCache.GameProfileInfo>)var9;
-        } catch (FileNotFoundException var7) {
-        } catch (JsonParseException | IOException var8) {
-            LOGGER.warn("Failed to load profile cache {}", this.file, var8);
+        } catch (FileNotFoundException filenotfoundexception) {
+            ;
+        // Spigot Start
+        } catch (com.google.gson.JsonSyntaxException | NullPointerException ex) {
+            GameProfileCache.LOGGER.warn( "Usercache.json is corrupted or has bad formatting. Deleting it to prevent further issues." );
+            this.file.delete();
+        // Spigot End
+        } catch (JsonParseException | IOException ioexception) {
+            GameProfileCache.LOGGER.warn("Failed to load profile cache {}", this.file, ioexception);
         }
 
-        return list;
+        return arraylist;
     }
 
-    public void save() {
-        JsonArray jsonArray = new JsonArray();
-        DateFormat dateFormat = createDateFormat();
-        this.getTopMRUProfiles(1000).forEach((entry) -> {
-            jsonArray.add(writeGameProfile(entry, dateFormat));
+    public void save(boolean asyncSave) { // Paper
+        JsonArray jsonarray = new JsonArray();
+        DateFormat dateformat = GameProfileCache.createDateFormat();
+
+        this.listTopMRUProfiles(org.spigotmc.SpigotConfig.userCacheCap).forEach((usercache_usercacheentry) -> { // Spigot // Paper - allow better concurrency
+            jsonarray.add(GameProfileCache.writeGameProfile(usercache_usercacheentry, dateformat));
         });
-        String string = this.gson.toJson((JsonElement)jsonArray);
+        String s = this.gson.toJson(jsonarray);
+        Runnable save = () -> { // Paper
 
         try {
-            Writer writer = Files.newWriter(this.file, StandardCharsets.UTF_8);
+            BufferedWriter bufferedwriter = Files.newWriter(this.file, StandardCharsets.UTF_8);
 
             try {
-                writer.write(string);
-            } catch (Throwable var8) {
-                if (writer != null) {
+                bufferedwriter.write(s);
+            } catch (Throwable throwable) {
+                if (bufferedwriter != null) {
                     try {
-                        writer.close();
-                    } catch (Throwable var7) {
-                        var8.addSuppressed(var7);
+                        bufferedwriter.close();
+                    } catch (Throwable throwable1) {
+                        throwable.addSuppressed(throwable1);
                     }
                 }
 
-                throw var8;
+                throw throwable;
             }
 
-            if (writer != null) {
-                writer.close();
+            if (bufferedwriter != null) {
+                bufferedwriter.close();
             }
-        } catch (IOException var9) {
+        } catch (IOException ioexception) {
+            ;
         }
+        // Paper start
+        };
+        if (asyncSave) {
+            net.minecraft.server.MCUtil.scheduleAsyncTask(save);
+        } else {
+            save.run();
+        }
+        // Paper end
 
     }
 
     private Stream<GameProfileCache.GameProfileInfo> getTopMRUProfiles(int limit) {
-        return ImmutableList.copyOf(this.profilesByUUID.values()).stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit((long)limit);
+        // Paper start - allow better concurrency
+        return this.listTopMRUProfiles(limit).stream();
     }
 
+    private List<GameProfileCache.GameProfileInfo> listTopMRUProfiles(int limit) {
+        try {
+            this.stateLock.lock();
+            return this.profilesByUUID.values().stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit(limit).toList();
+        } finally {
+            this.stateLock.unlock();
+        }
+    }
+    // Paper end
+
     private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo entry, DateFormat dateFormat) {
-        JsonObject jsonObject = new JsonObject();
-        jsonObject.addProperty("name", entry.getProfile().getName());
-        UUID uUID = entry.getProfile().getId();
-        jsonObject.addProperty("uuid", uUID == null ? "" : uUID.toString());
-        jsonObject.addProperty("expiresOn", dateFormat.format(entry.getExpirationDate()));
-        return jsonObject;
+        JsonObject jsonobject = new JsonObject();
+
+        jsonobject.addProperty("name", entry.getProfile().getName());
+        UUID uuid = entry.getProfile().getId();
+
+        jsonobject.addProperty("uuid", uuid == null ? "" : uuid.toString());
+        jsonobject.addProperty("expiresOn", dateFormat.format(entry.getExpirationDate()));
+        return jsonobject;
     }
 
     private static Optional<GameProfileCache.GameProfileInfo> readGameProfile(JsonElement json, DateFormat dateFormat) {
         if (json.isJsonObject()) {
-            JsonObject jsonObject = json.getAsJsonObject();
-            JsonElement jsonElement = jsonObject.get("name");
-            JsonElement jsonElement2 = jsonObject.get("uuid");
-            JsonElement jsonElement3 = jsonObject.get("expiresOn");
-            if (jsonElement != null && jsonElement2 != null) {
-                String string = jsonElement2.getAsString();
-                String string2 = jsonElement.getAsString();
+            JsonObject jsonobject = json.getAsJsonObject();
+            JsonElement jsonelement1 = jsonobject.get("name");
+            JsonElement jsonelement2 = jsonobject.get("uuid");
+            JsonElement jsonelement3 = jsonobject.get("expiresOn");
+
+            if (jsonelement1 != null && jsonelement2 != null) {
+                String s = jsonelement2.getAsString();
+                String s1 = jsonelement1.getAsString();
                 Date date = null;
-                if (jsonElement3 != null) {
+
+                if (jsonelement3 != null) {
                     try {
-                        date = dateFormat.parse(jsonElement3.getAsString());
-                    } catch (ParseException var12) {
+                        date = dateFormat.parse(jsonelement3.getAsString());
+                    } catch (ParseException parseexception) {
+                        ;
                     }
                 }
 
-                if (string2 != null && string != null && date != null) {
-                    UUID uUID;
+                if (s1 != null && s != null && date != null) {
+                    UUID uuid;
+
                     try {
-                        uUID = UUID.fromString(string);
-                    } catch (Throwable var11) {
+                        uuid = UUID.fromString(s);
+                    } catch (Throwable throwable) {
                         return Optional.empty();
                     }
 
-                    return Optional.of(new GameProfileCache.GameProfileInfo(new GameProfile(uUID, string2), date));
+                    return Optional.of(new GameProfileCache.GameProfileInfo(new GameProfile(uuid, s1), date));
                 } else {
                     return Optional.empty();
                 }
@@ -330,7 +415,8 @@
         }
     }
 
-    static class GameProfileInfo {
+    private static class GameProfileInfo {
+
         private final GameProfile profile;
         final Date expirationDate;
         private volatile long lastAccess;
