From e683012285b77215671dece3b02364899dac7a79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 04:05:15 +0000 Subject: [PATCH 1/4] Initial plan From 883cd843a17a125c511b2ccdd4e651319b944149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 04:11:25 +0000 Subject: [PATCH 2/4] Fix critical thread-safety issues in concurrent code Co-authored-by: kevinthegreat1 <92656833+kevinthegreat1@users.noreply.github.com> --- .../dungeon/secrets/SecretsTracker.java | 10 +++++--- .../item/tooltip/BackpackPreview.java | 25 +++++++++++-------- .../profileviewer/ProfileViewerScreen.java | 10 ++++---- .../utils/discord/DiscordRPCManager.java | 23 ++++++++++------- .../utils/ws/SkyblockerWebSocket.java | 7 +++++- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java index 481e3e6f787..9070ec4e3f5 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.regex.Matcher; import net.minecraft.client.Minecraft; @@ -70,7 +71,7 @@ private static void calculate(RunPhase phase) { case END -> CompletableFuture.runAsync(() -> { TrackedRun thisRun = currentRun; if (thisRun != null) { - Object2ObjectOpenHashMap secretsFound = new Object2ObjectOpenHashMap<>(); + ConcurrentHashMap secretsFound = new ConcurrentHashMap<>(); //Update secret counts for (Entry entry : thisRun.playersSecretData().entrySet()) { @@ -148,11 +149,12 @@ private static int getSecretCountFromAchievements(JsonObject playerJson) { } /** - * This will either reflect the value at the start or the end depending on when this is called + * This will either reflect the value at the start or the end depending on when this is called. + * Uses ConcurrentHashMap to ensure thread-safe access from multiple async tasks. */ - private record TrackedRun(Object2ObjectOpenHashMap playersSecretData) { + private record TrackedRun(ConcurrentHashMap playersSecretData) { private TrackedRun() { - this(new Object2ObjectOpenHashMap<>()); + this(new ConcurrentHashMap<>()); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java index 8eb6037e34f..22df988f527 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,7 +44,7 @@ public class BackpackPreview { private static final Pattern ECHEST_PATTERN = Pattern.compile("Ender Chest.*\\((\\d+)/\\d+\\)"); private static final Pattern BACKPACK_PATTERN = Pattern.compile("Backpack.*\\(Slot #(\\d+)\\)"); private static final int STORAGE_SIZE = 27; - private static final Storage[] storages = new Storage[STORAGE_SIZE]; + private static final AtomicReferenceArray storages = new AtomicReferenceArray<>(STORAGE_SIZE); /** * The profile id of the currently loaded backpack preview. @@ -88,7 +89,7 @@ public static void tick() { private static void loadStorages() { for (int index = 0; index < STORAGE_SIZE; ++index) { - storages[index] = null; + storages.set(index, null); //Copy variable since lambdas do not like when you use iteration variables (JDK-8300691) int index2 = index; @@ -104,7 +105,7 @@ private static void loadStorages() { } return null; - }, Executors.newVirtualThreadPerTaskExecutor()).thenAcceptAsync(storage -> storages[index2] = storage, Minecraft.getInstance()); + }, Executors.newVirtualThreadPerTaskExecutor()).thenAcceptAsync(storage -> storages.set(index2, storage), Minecraft.getInstance()); } } @@ -114,7 +115,8 @@ private static RegistryOps getOps() { private static void saveStorages() { for (int index = 0; index < STORAGE_SIZE; ++index) { - if (storages[index] != null && storages[index].dirty) { + Storage storage = storages.get(index); + if (storage != null && storage.dirty) { saveStorage(index); } } @@ -122,7 +124,7 @@ private static void saveStorages() { private static void saveStorage(int index) { //Store desired storage in a variable to ensure that the instance cannot change during async execution - Storage storage = storages[index]; + Storage storage = storages.get(index); CompletableFuture.runAsync(() -> { Path storageFile = saveDir.resolve(index + ".nbt"); @@ -138,7 +140,7 @@ private static void updateStorage(AbstractContainerScreen handledScreen) { String title = handledScreen.getTitle().getString(); int index = getStorageIndexFromTitle(title); if (index != -1) { - storages[index] = new Storage(handledScreen.getMenu().slots.getFirst().container, title, true); + storages.set(index, new Storage(handledScreen.getMenu().slots.getFirst().container, title, true)); } } @@ -147,8 +149,9 @@ public static boolean renderPreview(GuiGraphics context, Screen screen, int inde else if (index >= 27 && index < 45) index -= 18; else return false; - if (storages[index] == null) return false; - int rows = (storages[index].size() - 9) / 9; + Storage storage = storages.get(index); + if (storage == null) return false; + int rows = (storage.size() - 9) / 9; int x = mouseX + 184 >= screen.width ? mouseX - 188 : mouseX + 8; int y = Math.max(0, mouseY - 16); @@ -157,10 +160,10 @@ public static boolean renderPreview(GuiGraphics context, Screen screen, int inde context.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, x, y + rows * 18 + 17, 0, 215, 176, 7, 256, 256); Font textRenderer = Minecraft.getInstance().font; - context.drawString(textRenderer, storages[index].name(), x + 8, y + 6, 0xFF404040, false); + context.drawString(textRenderer, storage.name(), x + 8, y + 6, 0xFF404040, false); - for (int i = 9; i < storages[index].size(); ++i) { - ItemStack currentStack = storages[index].getStack(i); + for (int i = 9; i < storage.size(); ++i) { + ItemStack currentStack = storage.getStack(i); int itemX = x + (i - 9) % 9 * 18 + 8; int itemY = y + (i - 9) / 9 * 18 + 18; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java index 9b9d2ddcbe6..843faf7e1b6 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java @@ -61,16 +61,16 @@ public class ProfileViewerScreen extends Screen { private static Map> collections = Map.of(); private String playerName; - private JsonObject hypixelProfile; - private JsonObject playerProfile; - private boolean profileNotFound = false; - private String errorMessage = "No Profile"; + private volatile JsonObject hypixelProfile; + private volatile JsonObject playerProfile; + private volatile boolean profileNotFound = false; + private volatile String errorMessage = "No Profile"; private int activePage = 0; private static final String[] PAGE_NAMES = {"Skills", "Slayers", "Dungeons", "Inventories", "Collections"}; private final ProfileViewerPage[] profileViewerPages = new ProfileViewerPage[PAGE_NAMES.length]; private final List profileViewerNavButtons = new ArrayList<>(); - private RemotePlayer entity; + private volatile RemotePlayer entity; private ProfileViewerTextWidget textWidget; public ProfileViewerScreen(String username) { diff --git a/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java index 35107f4407e..0fcd7a350a5 100644 --- a/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java @@ -14,6 +14,9 @@ import java.text.DecimalFormat; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; /** * Manages the discord rich presence. Automatically connects to discord and displays a customizable activity when playing Skyblock. @@ -24,15 +27,15 @@ public class DiscordRPCManager { /** * The update task used to avoid multiple update tasks running simultaneously. */ - public static CompletableFuture updateTask; - public static long startTimeStamp; - public static int cycleCount; + private static final AtomicReference> updateTask = new AtomicReference<>(); + private static final AtomicLong startTimeStamp = new AtomicLong(); + private static final AtomicInteger cycleCount = new AtomicInteger(); @Init public static void init() { SkyblockEvents.LEAVE.register(DiscordRPCManager::initAndUpdatePresence); SkyblockEvents.JOIN.register(() -> { - startTimeStamp = System.currentTimeMillis(); + startTimeStamp.set(System.currentTimeMillis()); initAndUpdatePresence(true); }); } @@ -45,7 +48,7 @@ public static void updateDataAndPresence() { if (SkyblockerConfigManager.get().misc.richPresence.customMessage.isEmpty()) { SkyblockerConfigManager.update(config -> config.misc.richPresence.customMessage = "Playing Skyblock"); } - if (SkyblockerConfigManager.get().misc.richPresence.cycleMode) cycleCount = (cycleCount + 1) % 3; + if (SkyblockerConfigManager.get().misc.richPresence.cycleMode) cycleCount.updateAndGet(count -> (count + 1) % 3); initAndUpdatePresence(); } @@ -73,8 +76,9 @@ private static void initAndUpdatePresence() { * if {@link MiscConfig.RichPresence#enableRichPresence rich presence is disabled}. */ private static void initAndUpdatePresence(boolean initialization) { - if (updateTask == null || updateTask.isDone()) { - updateTask = CompletableFuture.runAsync(() -> { + CompletableFuture currentTask = updateTask.get(); + if (currentTask == null || currentTask.isDone()) { + CompletableFuture newTask = CompletableFuture.runAsync(() -> { if (SkyblockerConfigManager.get().misc.richPresence.enableRichPresence && Utils.isOnSkyblock()) { if (!DiscordIPC.isConnected()) { if (DiscordIPC.start(934607927837356052L, null)) { @@ -94,13 +98,14 @@ private static void initAndUpdatePresence(boolean initialization) { LOGGER.info("[Skyblocker] Discord RPC is currently disabled, will not connect"); } }, Executors.newVirtualThreadPerTaskExecutor()); + updateTask.set(newTask); } } public static RichPresence buildPresence() { RichPresence presence = new RichPresence(); presence.setLargeImage("skyblocker-default", null); - presence.setStart(startTimeStamp); + presence.setStart(startTimeStamp.get()); presence.setDetails(SkyblockerConfigManager.get().misc.richPresence.customMessage); presence.setState(getInfo()); return presence; @@ -115,7 +120,7 @@ public static String getInfo() { case LOCATION -> info = Utils.getIslandArea(); } } else if (SkyblockerConfigManager.get().misc.richPresence.cycleMode) { - switch (cycleCount) { + switch (cycleCount.get()) { case 0 -> info = "Bits: " + DECIMAL_FORMAT.format(Utils.getBits()); case 1 -> info = "Purse: " + DECIMAL_FORMAT.format(Utils.getPurse()); case 2 -> info = Utils.getIslandArea(); diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java b/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java index f9f7793bd1f..d14f3e1e5fd 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java +++ b/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java @@ -78,7 +78,12 @@ private static void sendInternal(String message) { try { if (Debug.debugEnabled() && Debug.webSocketDebug()) LOGGER.info("[Skyblocker WebSocket] Sending Message: {}", message); - socket.sendText(message, true).join(); + WebSocket currentSocket = socket; + if (currentSocket != null && !currentSocket.isOutputClosed()) { + currentSocket.sendText(message, true).join(); + } else { + LOGGER.warn("[Skyblocker WebSocket] Cannot send message - socket is null or closed"); + } } catch (Exception e) { LOGGER.error("[Skyblocker WebSocket] Failed to send message!", e); } From 0bca7ef378dd74e27282681e648705dd8460be0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 04:12:15 +0000 Subject: [PATCH 3/4] Fix ItemRepository volatile flags for thread visibility Co-authored-by: kevinthegreat1 <92656833+kevinthegreat1@users.noreply.github.com> --- .../de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java index d8c3b0d45c1..36b6cdf0fbf 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java @@ -57,11 +57,11 @@ private record AfterImportTask(Runnable runnable, boolean async) {} /** * Consumers must check this field when accessing `items` and `itemsMap`, or else thread safety is not guaranteed. */ - private static boolean itemsImported = false; + private static volatile boolean itemsImported = false; /** * Consumers must check this field when accessing `recipes`, or else thread safety is not guaranteed. */ - private static boolean filesImported = false; + private static volatile boolean filesImported = false; @Init public static void init() { From 11a3eaaba0126cb7a34527b5d660ece6e49b883c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:49:38 +0000 Subject: [PATCH 4/4] Revert unnecessary thread-safety changes to code not accessed concurrently Co-authored-by: kevinthegreat1 <92656833+kevinthegreat1@users.noreply.github.com> --- .../dungeon/secrets/SecretsTracker.java | 10 ++++---- .../utils/discord/DiscordRPCManager.java | 23 ++++++++----------- .../utils/ws/SkyblockerWebSocket.java | 7 +----- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java index 9070ec4e3f5..481e3e6f787 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java @@ -18,7 +18,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.regex.Matcher; import net.minecraft.client.Minecraft; @@ -71,7 +70,7 @@ private static void calculate(RunPhase phase) { case END -> CompletableFuture.runAsync(() -> { TrackedRun thisRun = currentRun; if (thisRun != null) { - ConcurrentHashMap secretsFound = new ConcurrentHashMap<>(); + Object2ObjectOpenHashMap secretsFound = new Object2ObjectOpenHashMap<>(); //Update secret counts for (Entry entry : thisRun.playersSecretData().entrySet()) { @@ -149,12 +148,11 @@ private static int getSecretCountFromAchievements(JsonObject playerJson) { } /** - * This will either reflect the value at the start or the end depending on when this is called. - * Uses ConcurrentHashMap to ensure thread-safe access from multiple async tasks. + * This will either reflect the value at the start or the end depending on when this is called */ - private record TrackedRun(ConcurrentHashMap playersSecretData) { + private record TrackedRun(Object2ObjectOpenHashMap playersSecretData) { private TrackedRun() { - this(new ConcurrentHashMap<>()); + this(new Object2ObjectOpenHashMap<>()); } } diff --git a/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java index 0fcd7a350a5..35107f4407e 100644 --- a/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java @@ -14,9 +14,6 @@ import java.text.DecimalFormat; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; /** * Manages the discord rich presence. Automatically connects to discord and displays a customizable activity when playing Skyblock. @@ -27,15 +24,15 @@ public class DiscordRPCManager { /** * The update task used to avoid multiple update tasks running simultaneously. */ - private static final AtomicReference> updateTask = new AtomicReference<>(); - private static final AtomicLong startTimeStamp = new AtomicLong(); - private static final AtomicInteger cycleCount = new AtomicInteger(); + public static CompletableFuture updateTask; + public static long startTimeStamp; + public static int cycleCount; @Init public static void init() { SkyblockEvents.LEAVE.register(DiscordRPCManager::initAndUpdatePresence); SkyblockEvents.JOIN.register(() -> { - startTimeStamp.set(System.currentTimeMillis()); + startTimeStamp = System.currentTimeMillis(); initAndUpdatePresence(true); }); } @@ -48,7 +45,7 @@ public static void updateDataAndPresence() { if (SkyblockerConfigManager.get().misc.richPresence.customMessage.isEmpty()) { SkyblockerConfigManager.update(config -> config.misc.richPresence.customMessage = "Playing Skyblock"); } - if (SkyblockerConfigManager.get().misc.richPresence.cycleMode) cycleCount.updateAndGet(count -> (count + 1) % 3); + if (SkyblockerConfigManager.get().misc.richPresence.cycleMode) cycleCount = (cycleCount + 1) % 3; initAndUpdatePresence(); } @@ -76,9 +73,8 @@ private static void initAndUpdatePresence() { * if {@link MiscConfig.RichPresence#enableRichPresence rich presence is disabled}. */ private static void initAndUpdatePresence(boolean initialization) { - CompletableFuture currentTask = updateTask.get(); - if (currentTask == null || currentTask.isDone()) { - CompletableFuture newTask = CompletableFuture.runAsync(() -> { + if (updateTask == null || updateTask.isDone()) { + updateTask = CompletableFuture.runAsync(() -> { if (SkyblockerConfigManager.get().misc.richPresence.enableRichPresence && Utils.isOnSkyblock()) { if (!DiscordIPC.isConnected()) { if (DiscordIPC.start(934607927837356052L, null)) { @@ -98,14 +94,13 @@ private static void initAndUpdatePresence(boolean initialization) { LOGGER.info("[Skyblocker] Discord RPC is currently disabled, will not connect"); } }, Executors.newVirtualThreadPerTaskExecutor()); - updateTask.set(newTask); } } public static RichPresence buildPresence() { RichPresence presence = new RichPresence(); presence.setLargeImage("skyblocker-default", null); - presence.setStart(startTimeStamp.get()); + presence.setStart(startTimeStamp); presence.setDetails(SkyblockerConfigManager.get().misc.richPresence.customMessage); presence.setState(getInfo()); return presence; @@ -120,7 +115,7 @@ public static String getInfo() { case LOCATION -> info = Utils.getIslandArea(); } } else if (SkyblockerConfigManager.get().misc.richPresence.cycleMode) { - switch (cycleCount.get()) { + switch (cycleCount) { case 0 -> info = "Bits: " + DECIMAL_FORMAT.format(Utils.getBits()); case 1 -> info = "Purse: " + DECIMAL_FORMAT.format(Utils.getPurse()); case 2 -> info = Utils.getIslandArea(); diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java b/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java index d14f3e1e5fd..f9f7793bd1f 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java +++ b/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java @@ -78,12 +78,7 @@ private static void sendInternal(String message) { try { if (Debug.debugEnabled() && Debug.webSocketDebug()) LOGGER.info("[Skyblocker WebSocket] Sending Message: {}", message); - WebSocket currentSocket = socket; - if (currentSocket != null && !currentSocket.isOutputClosed()) { - currentSocket.sendText(message, true).join(); - } else { - LOGGER.warn("[Skyblocker WebSocket] Cannot send message - socket is null or closed"); - } + socket.sendText(message, true).join(); } catch (Exception e) { LOGGER.error("[Skyblocker WebSocket] Failed to send message!", e); }