diff --git a/commons/src/main/java/net/swofty/commons/redis/ProxyHeartbeat.java b/commons/src/main/java/net/swofty/commons/redis/ProxyHeartbeat.java
new file mode 100644
index 000000000..5af89ff6d
--- /dev/null
+++ b/commons/src/main/java/net/swofty/commons/redis/ProxyHeartbeat.java
@@ -0,0 +1,49 @@
+package net.swofty.commons.redis;
+
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+
+/**
+ * Liveness signalling between the Velocity proxy and the game servers.
+ *
+ *
The old design had every game server send a {@code ProxyIsOnlineProtocol}
+ * request/response to the proxy once per second and shut down on a single missed
+ * reply within ~1s. Under many concurrent servers that round-trip regularly
+ * exceeded the window and killed healthy servers.
+ *
+ *
This replaces it with a push/expiry model: the proxy refreshes a single
+ * short-lived Redis key on a fixed interval (one write regardless of how many
+ * servers exist). Game servers simply read the key — if it is absent (the proxy
+ * stopped refreshing it and Redis let it expire) for several consecutive checks,
+ * the proxy is considered dead. No per-server request/response, no coupling to
+ * the game tick loop, and transient Redis latency is harmless.
+ */
+public final class ProxyHeartbeat {
+ private static final String KEY = "proxy:heartbeat";
+ /** How long a single beat keeps the key alive. Must exceed the beat interval. */
+ private static final int TTL_SECONDS = 6;
+
+ private static volatile JedisPool pool;
+
+ private ProxyHeartbeat() {}
+
+ public static synchronized void init(String redisUri) {
+ if (pool == null) {
+ pool = RedisConnectionPool.connect(redisUri, RedisConnectionPool.Settings.standard());
+ }
+ }
+
+ /** Proxy: call on a fixed interval shorter than {@link #TTL_SECONDS} to advertise liveness. */
+ public static void beat() {
+ try (Jedis jedis = pool.getResource()) {
+ jedis.setex(KEY, TTL_SECONDS, Long.toString(System.currentTimeMillis()));
+ }
+ }
+
+ /** Game server: true while the proxy is refreshing its heartbeat key. */
+ public static boolean isProxyAlive() {
+ try (Jedis jedis = pool.getResource()) {
+ return jedis.exists(KEY);
+ }
+ }
+}
diff --git a/commons/src/main/java/net/swofty/commons/skyblock/auctions/AuctionItem.java b/commons/src/main/java/net/swofty/commons/skyblock/auctions/AuctionItem.java
index f52d24403..9436cbcd7 100644
--- a/commons/src/main/java/net/swofty/commons/skyblock/auctions/AuctionItem.java
+++ b/commons/src/main/java/net/swofty/commons/skyblock/auctions/AuctionItem.java
@@ -1,8 +1,11 @@
package net.swofty.commons.skyblock.auctions;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.Setter;
import net.swofty.commons.skyblock.item.UnderstandableSkyBlockItem;
+import net.swofty.commons.protocol.serializers.AuctionItemSerializer;
import net.swofty.commons.protocol.serializers.UnderstandableSkyBlockItemSerializer;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
@@ -39,6 +42,23 @@ public AuctionItem(UnderstandableSkyBlockItem item, UUID originator, long endTim
this.bids = new ArrayList<>();
}
+ /**
+ * Jackson round-trips an AuctionItem as its purpose-built serialized string rather than as a
+ * bean graph. The graph contains a {@link UnderstandableSkyBlockItem} whose {@code attributes}
+ * are abstract {@link net.swofty.commons.skyblock.item.attribute.ItemAttribute}s, which Jackson
+ * cannot reconstruct (no concrete type, no default creator). Delegating to
+ * {@link AuctionItemSerializer} reuses the same key-based format used for MongoDB storage.
+ */
+ @JsonValue
+ public String toSerializedString() {
+ return new AuctionItemSerializer<>().serialize(this);
+ }
+
+ @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
+ public static AuctionItem fromSerializedString(String json) {
+ return new AuctionItemSerializer<>().deserialize(json);
+ }
+
public Document toDocument() {
return new Document()
.append("_id", uuid.toString())
diff --git a/configuration/i18n/en_US/tablist.properties b/configuration/i18n/en_US/tablist.properties
index dc7461da7..5d912b23f 100644
--- a/configuration/i18n/en_US/tablist.properties
+++ b/configuration/i18n/en_US/tablist.properties
@@ -27,8 +27,8 @@ tablist.skywars_game.footer.store = Ranks, Boosters & MORE! ST
# Module Headers
tablist.module.server_info = Server Info
-tablist.module.players=Players ({})
-tablist.module.players_murder_mystery=Players (})
+tablist.module.players=Players ()
+tablist.module.players_murder_mystery=Players ()
tablist.module.account_info = Account Info
tablist.module.island=Island ()
tablist.module.guests = Guests
diff --git a/loader/src/main/java/net/swofty/loader/Hypixel.java b/loader/src/main/java/net/swofty/loader/Hypixel.java
index d91f637e6..26459f49c 100644
--- a/loader/src/main/java/net/swofty/loader/Hypixel.java
+++ b/loader/src/main/java/net/swofty/loader/Hypixel.java
@@ -21,6 +21,7 @@
import net.swofty.commons.config.ConfigProvider;
import net.swofty.commons.protocol.RedisProtocol;
import net.swofty.commons.protocol.objects.proxy.to.*;
+import net.swofty.commons.redis.ProxyHeartbeat;
import net.swofty.commons.redis.RedisClient;
import net.swofty.proxyapi.ProxyAPI;
import net.swofty.proxyapi.ProxyService;
@@ -48,8 +49,11 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -246,7 +250,7 @@ static void main(String[] args) {
Logger.info("Received server name: " + HypixelConst.getServerName());
});
- checkProxyConnected(MinecraftServer.getSchedulerManager());
+ checkProxyConnected();
// Initialize anticheat
if (ConfigProvider.settings().getIntegrations().isAnticheat()) {
@@ -340,32 +344,47 @@ private static Map parseOptionalArgs(String[] args) {
return options;
}
- private static void checkProxyConnected(Scheduler scheduler) {
- scheduler.submitTask(() -> {
- AtomicBoolean responded = new AtomicBoolean(false);
+ // How often to read the proxy heartbeat key, and how many consecutive absences
+ // to tolerate before declaring the proxy dead. 3 misses x 3s = ~9s of confirmed
+ // silence on top of the key's 6s TTL — resilient to transient Redis/GC blips,
+ // while still cleaning up genuinely orphaned servers promptly.
+ private static final int PROXY_HEARTBEAT_CHECK_SECONDS = 3;
+ private static final int PROXY_HEARTBEAT_MAX_MISSES = 3;
+ private static void checkProxyConnected() {
+ ProxyHeartbeat.init(ConfigProvider.settings().getRedisUri());
+
+ ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(runnable -> {
+ Thread thread = new Thread(runnable, "proxy-heartbeat-monitor");
+ thread.setDaemon(true);
+ return thread;
+ });
+
+ AtomicInteger missed = new AtomicInteger(0);
+ monitor.scheduleAtFixedRate(() -> {
+ boolean alive;
try {
- RedisClient.requestProxy(new ProxyIsOnlineProtocol(),
- new ProxyIsOnlineProtocol.Request()).thenAccept(response -> {
- if (response.online()) {
- responded.set(true);
- }
- });
+ alive = ProxyHeartbeat.isProxyAlive();
} catch (Exception e) {
- MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(player -> player.kick("§cServer has lost connection to the proxy, please rejoin"));
- CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS)
- .execute(() -> System.exit(0));
- return TaskSchedule.stop();
+ // A Redis hiccup is treated as a miss, but never a fatal one on its own.
+ alive = false;
}
- scheduler.scheduleTask(() -> {
- if (!responded.get()) {
- Logger.error("Proxy did not respond to alive check. Shutting down...");
- System.exit(0);
- }
- }, TaskSchedule.tick(20), TaskSchedule.stop());
+ if (alive) {
+ missed.set(0);
+ return;
+ }
- return TaskSchedule.seconds(1);
- }, ExecutionType.TICK_END);
+ int misses = missed.incrementAndGet();
+ Logger.warn("Proxy heartbeat missing ({}/{})", misses, PROXY_HEARTBEAT_MAX_MISSES);
+ if (misses >= PROXY_HEARTBEAT_MAX_MISSES) {
+ Logger.error("Proxy heartbeat absent for ~{}s. Shutting down...",
+ PROXY_HEARTBEAT_MAX_MISSES * PROXY_HEARTBEAT_CHECK_SECONDS);
+ MinecraftServer.getConnectionManager().getOnlinePlayers()
+ .forEach(player -> player.kick("§cServer has lost connection to the proxy, please rejoin"));
+ CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS)
+ .execute(() -> System.exit(0));
+ }
+ }, PROXY_HEARTBEAT_CHECK_SECONDS, PROXY_HEARTBEAT_CHECK_SECONDS, TimeUnit.SECONDS);
}
}
diff --git a/service.auctionhouse/src/main/java/net/swofty/service/auction/AuctionService.java b/service.auctionhouse/src/main/java/net/swofty/service/auction/AuctionService.java
index 390689b9a..8f93c624c 100644
--- a/service.auctionhouse/src/main/java/net/swofty/service/auction/AuctionService.java
+++ b/service.auctionhouse/src/main/java/net/swofty/service/auction/AuctionService.java
@@ -11,12 +11,15 @@ public class AuctionService implements SkyBlockService {
public static AuctionsCacheService cacheService;
static void main(String[] args) {
- SkyBlockService.init(new AuctionService());
+ // SkyBlockService.init(...) blocks forever (it awaits a latch), so every dependency the
+ // endpoints touch must be initialised BEFORE it is called. Databases first (the cache reads
+ // their static collections), then the cache, then start handling messages.
+ new AuctionActiveDatabase("_placeholder").connect(ConfigProvider.settings().getMongodb());
+ new AuctionInactiveDatabase("_placeholder").connect(ConfigProvider.settings().getMongodb());
cacheService = new AuctionsCacheService();
- new AuctionActiveDatabase("_placeholder").connect(ConfigProvider.settings().getMongodb());
- new AuctionInactiveDatabase("_placeholder").connect(ConfigProvider.settings().getMongodb());
+ SkyBlockService.init(new AuctionService());
}
@Override
diff --git a/service.auctionhouse/src/main/java/net/swofty/service/auction/endpoints/EndpointFetchItems.java b/service.auctionhouse/src/main/java/net/swofty/service/auction/endpoints/EndpointFetchItems.java
index 07ff7d5c7..4a5028707 100644
--- a/service.auctionhouse/src/main/java/net/swofty/service/auction/endpoints/EndpointFetchItems.java
+++ b/service.auctionhouse/src/main/java/net/swofty/service/auction/endpoints/EndpointFetchItems.java
@@ -66,6 +66,17 @@ public AuctionFetchItemsProtocol.AuctionFetchItemsResponse handle(AuctionFetchIt
break;
}
- return new AuctionFetchItemsProtocol.AuctionFetchItemsResponse(results.stream().map(AuctionItem::fromDocument).toList(), true, null);
+ // Deserialize each document defensively: one malformed stored item should not
+ // take down the whole category fetch.
+ List items = new ArrayList<>(results.size());
+ for (Document document : results) {
+ try {
+ items.add(AuctionItem.fromDocument(document));
+ } catch (Exception e) {
+ System.err.println("Skipping unreadable auction _id=" + document.get("_id") + " in category " + category);
+ e.printStackTrace();
+ }
+ }
+ return new AuctionFetchItemsProtocol.AuctionFetchItemsResponse(items, true, null);
}
}
diff --git a/service.orchestrator/src/main/java/net/swofty/service/orchestrator/OrchestratorService.java b/service.orchestrator/src/main/java/net/swofty/service/orchestrator/OrchestratorService.java
index e0a81b0db..388b687aa 100644
--- a/service.orchestrator/src/main/java/net/swofty/service/orchestrator/OrchestratorService.java
+++ b/service.orchestrator/src/main/java/net/swofty/service/orchestrator/OrchestratorService.java
@@ -13,8 +13,8 @@
public class OrchestratorService implements SkyBlockService {
static void main() {
- SkyBlockService.init(new OrchestratorService());
-
+ // SkyBlockService.init(...) blocks forever, so schedule the cleanup task before calling it,
+ // otherwise the scheduler is never started.
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "orchestrator-cleanup");
t.setDaemon(true);
@@ -22,6 +22,8 @@ static void main() {
});
scheduler.scheduleAtFixedRate(OrchestratorCache::cleanup, 5, 5, TimeUnit.SECONDS);
Logger.info("Started orchestrator service");
+
+ SkyBlockService.init(new OrchestratorService());
}
@Override
diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/gui/ActionPlayerInventoryClick.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/gui/ActionPlayerInventoryClick.java
index 004847cd4..5f270093a 100644
--- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/gui/ActionPlayerInventoryClick.java
+++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/gui/ActionPlayerInventoryClick.java
@@ -40,8 +40,11 @@ public void handle(InventoryPreClickEvent event, final HypixelPlayer player) {
ItemStack clickedItem = event.getClickedItem();
ItemStack cursorItem = player.getInventory().getCursorItem();
- // Check for offhand
- if (event.getSlot() == 45) {
+ // Cancel interaction with the offhand slot, but ONLY in the player's own
+ // inventory. Slot 45 is the offhand there, yet in an open chest GUI (e.g. a
+ // CHEST_6_ROW) it is a normal, clickable slot — the 6th auction category button
+ // lives there, and blanket-cancelling slot 45 swallowed its clicks entirely.
+ if (event.getSlot() == 45 && event.getInventory() instanceof PlayerInventory) {
event.setCancelled(true);
return;
}
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionBrowser.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionBrowser.java
index c05580870..d4b18d617 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionBrowser.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionBrowser.java
@@ -28,6 +28,7 @@
import net.swofty.type.generic.i18n.I18n;
import net.swofty.type.generic.user.HypixelPlayer;
import net.swofty.type.generic.utility.PaginationList;
+import org.tinylog.Logger;
import net.swofty.type.skyblockgeneric.auction.AuctionItemLoreHandler;
import net.swofty.type.skyblockgeneric.item.SkyBlockItem;
import net.swofty.type.skyblockgeneric.item.updater.PlayerItemUpdater;
@@ -82,6 +83,10 @@ private void updateItemsCache() {
// Set the items in the GUI
List paginatedItems = paginationList.getPage(page);
setItemCache(paginatedItems);
+ })
+ .exceptionally(ex -> {
+ Logger.error(ex, "Auction browse failed for category {}", category);
+ return null;
});
}
@@ -276,12 +281,14 @@ public void onBottomClick(InventoryPreClickEvent e) {
@Override
public void refreshItems(HypixelPlayer player) {
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join()) {
- player.sendMessage(I18n.t("gui_auction.browser.offline_message"));
- player.closeInventory();
- }
-
- setItems();
+ new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().thenAccept(online -> {
+ if (!online) {
+ player.sendMessage(I18n.t("gui_auction.browser.offline_message"));
+ player.closeInventory();
+ return;
+ }
+ setItems();
+ });
}
@Override
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionCreateItem.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionCreateItem.java
index 383d3d9db..7e6b7414e 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionCreateItem.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionCreateItem.java
@@ -169,12 +169,15 @@ public void run(InventoryPreClickEvent e, HypixelPlayer p) {
AuctionAddItemProtocol.AuctionAddItemMessage message =
new AuctionAddItemProtocol.AuctionAddItemMessage(item, category);
+ // Never block here: this callback runs on the Redis response thread, and the
+ // add-item reply arrives on that same thread — a join() would deadlock forever.
CompletableFuture future =
auctionService.handleRequest(message);
- UUID auctionUUID = future.join().uuid();
-
- player.sendMessage(I18n.t("gui_auction.create.started_message", Component.text(itemName)));
- player.sendMessage(I18n.t("gui_auction.create.started_id", Component.text(auctionUUID.toString())));
+ future.thenAccept(addResponse -> {
+ UUID auctionUUID = addResponse.uuid();
+ player.sendMessage(I18n.t("gui_auction.create.started_message", Component.text(itemName)));
+ player.sendMessage(I18n.t("gui_auction.create.started_id", Component.text(auctionUUID.toString())));
+ });
});
}
@@ -278,10 +281,12 @@ public void onBottomClick(InventoryPreClickEvent e) {
@Override
public void refreshItems(HypixelPlayer player) {
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join()) {
- player.sendMessage(I18n.t("gui_auction.create.offline_message"));
- player.closeInventory();
- }
+ new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().thenAccept(online -> {
+ if (!online) {
+ player.sendMessage(I18n.t("gui_auction.create.offline_message"));
+ player.closeInventory();
+ }
+ });
}
@Override
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionHouse.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionHouse.java
index 56e6c7373..73896d764 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionHouse.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionHouse.java
@@ -21,9 +21,8 @@
public class GUIAuctionHouse extends HypixelInventoryGUI implements RefreshingGUI {
public GUIAuctionHouse() {
super(I18n.t("gui_auction.house.title"), InventoryType.CHEST_4_ROW);
-
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join())
- fill(Material.BLACK_STAINED_GLASS_PANE, "");
+ // onOpen paints the panes and refreshItems() handles the offline case async — never
+ // block the tick thread on an isOnline() round-trip here.
}
@Override
@@ -133,10 +132,12 @@ public void onBottomClick(InventoryPreClickEvent e) {
@Override
public void refreshItems(HypixelPlayer player) {
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join()) {
- player.sendMessage(I18n.t("gui_auction.house.offline_message"));
- player.closeInventory();
- }
+ new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().thenAccept(online -> {
+ if (!online) {
+ player.sendMessage(I18n.t("gui_auction.house.offline_message"));
+ player.closeInventory();
+ }
+ });
}
@Override
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionViewItem.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionViewItem.java
index 3b2865826..b3fc34269 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionViewItem.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIAuctionViewItem.java
@@ -52,48 +52,51 @@ public void onOpen(InventoryGUIOpenEvent e) {
public void updateItems() {
AuctionFetchItemProtocol.AuctionFetchItemMessage message =
new AuctionFetchItemProtocol.AuctionFetchItemMessage(auctionID);
- CompletableFuture future =
- new ProxyService(ServiceType.AUCTION_HOUSE).handleRequest(message);
- AuctionItem item = future.join().item();
-
- set(GUIClickableItem.getGoBackItem(49, previousGUI));
-
- set(new GUIItem(13) {
- @Override
- public ItemStack.Builder getItem(HypixelPlayer p) {
- SkyBlockPlayer player = (SkyBlockPlayer) p;
- return ItemStackCreator.updateLore(
- PlayerItemUpdater.playerUpdate(player, new SkyBlockItem(item.getItem()).getItemStack()),
- new AuctionItemLoreHandler(item).getLore(player)
- );
+ // Fully async: never block on the reply (which arrives on the Redis thread).
+ new ProxyService(ServiceType.AUCTION_HOUSE).handleRequest(message).thenAccept(response -> {
+ AuctionItem item = ((AuctionFetchItemProtocol.AuctionFetchItemResponse) response).item();
+
+ set(GUIClickableItem.getGoBackItem(49, previousGUI));
+
+ set(new GUIItem(13) {
+ @Override
+ public ItemStack.Builder getItem(HypixelPlayer p) {
+ SkyBlockPlayer player = (SkyBlockPlayer) p;
+ return ItemStackCreator.updateLore(
+ PlayerItemUpdater.playerUpdate(player, new SkyBlockItem(item.getItem()).getItemStack()),
+ new AuctionItemLoreHandler(item).getLore(player)
+ );
+ }
+ });
+
+ if (!item.getOriginator().equals(getPlayer().getUuid())) {
+ if (item.isBin()) {
+ new AuctionViewThirdBin().open(this, item, (SkyBlockPlayer) getPlayer());
+ return;
+ }
+
+ new AuctionViewThirdNormal().open(this, item, (SkyBlockPlayer) getPlayer());
+ } else {
+ if (item.isBin()) {
+ new AuctionViewSelfBIN().open(this, item, (SkyBlockPlayer) getPlayer());
+ return;
+ }
+
+ new AuctionViewSelfNormal().open(this, item, (SkyBlockPlayer) getPlayer());
}
});
-
- if (!item.getOriginator().equals(getPlayer().getUuid())) {
- if (item.isBin()) {
- new AuctionViewThirdBin().open(this, item, (SkyBlockPlayer) getPlayer());
- return;
- }
-
- new AuctionViewThirdNormal().open(this, item, (SkyBlockPlayer) getPlayer());
- } else {
- if (item.isBin()) {
- new AuctionViewSelfBIN().open(this, item, (SkyBlockPlayer) getPlayer());
- return;
- }
-
- new AuctionViewSelfNormal().open(this, item, (SkyBlockPlayer) getPlayer());
- }
}
@Override
public void refreshItems(HypixelPlayer player) {
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join()) {
- player.sendMessage(I18n.t("gui_auction.view.offline_message"));
- player.closeInventory();
- }
-
- updateItems();
+ new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().thenAccept(online -> {
+ if (!online) {
+ player.sendMessage(I18n.t("gui_auction.view.offline_message"));
+ player.closeInventory();
+ return;
+ }
+ updateItems();
+ });
}
@Override
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIManageAuctions.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIManageAuctions.java
index 1ff586d33..aaf26011f 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIManageAuctions.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIManageAuctions.java
@@ -82,46 +82,47 @@ public void setItems() {
futures.add(future);
});
+ // Render the page once every fetch has resolved — without blocking the caller.
CompletableFuture allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
- allDone.join();
-
- // Sort the items by the time they were added
- auctionItems.sort((o1, o2) -> Long.compare(o2.getEndTime(), o1.getEndTime()));
-
- List auctionItemsPage = auctionItems.getPage(1);
+ allDone.thenRun(() -> {
+ // Sort the items by the time they were added
+ auctionItems.sort((o1, o2) -> Long.compare(o2.getEndTime(), o1.getEndTime()));
+
+ List auctionItemsPage = auctionItems.getPage(1);
+
+ for (int i = 0; i < 7; i++) {
+ int slot = i + 10;
+
+ if (i >= auctionItems.size()) {
+ set(new GUIItem(slot) {
+ @Override
+ public ItemStack.Builder getItem(HypixelPlayer p) {
+ SkyBlockPlayer player = (SkyBlockPlayer) p;
+ return ItemStack.builder(Material.AIR);
+ }
+ });
+ continue;
+ }
- for (int i = 0; i < 7; i++) {
- int slot = i + 10;
+ AuctionItem item = auctionItemsPage.get(i);
+ set(new GUIClickableItem(slot) {
+ @Override
+ public void run(InventoryPreClickEvent e, HypixelPlayer p) {
+ SkyBlockPlayer player = (SkyBlockPlayer) p;
+ new GUIAuctionViewItem(item.getUuid(), GUIManageAuctions.this).open(player);
+ }
- if (i >= auctionItems.size()) {
- set(new GUIItem(slot) {
@Override
public ItemStack.Builder getItem(HypixelPlayer p) {
SkyBlockPlayer player = (SkyBlockPlayer) p;
- return ItemStack.builder(Material.AIR);
+ return ItemStackCreator.getStack(
+ StringUtility.getTextFromComponent(new NonPlayerItemUpdater(item.getItem()).getUpdatedItem().build()
+ .get(DataComponents.CUSTOM_NAME)),
+ item.getItem().material(), item.getItem().amount(), new AuctionItemLoreHandler(item).getLore(player));
}
});
- continue;
}
-
- AuctionItem item = auctionItemsPage.get(i);
- set(new GUIClickableItem(slot) {
- @Override
- public void run(InventoryPreClickEvent e, HypixelPlayer p) {
- SkyBlockPlayer player = (SkyBlockPlayer) p;
- new GUIAuctionViewItem(item.getUuid(), GUIManageAuctions.this).open(player);
- }
-
- @Override
- public ItemStack.Builder getItem(HypixelPlayer p) {
- SkyBlockPlayer player = (SkyBlockPlayer) p;
- return ItemStackCreator.getStack(
- StringUtility.getTextFromComponent(new NonPlayerItemUpdater(item.getItem()).getUpdatedItem().build()
- .get(DataComponents.CUSTOM_NAME)),
- item.getItem().material(), item.getItem().amount(), new AuctionItemLoreHandler(item).getLore(player));
- }
- });
- }
+ });
}
@Override
@@ -146,13 +147,14 @@ public void onBottomClick(InventoryPreClickEvent e) {
@Override
public void refreshItems(HypixelPlayer player) {
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join()) {
- player.sendMessage(I18n.t("gui_auction.manage.offline_message"));
- player.closeInventory();
- return;
- }
-
- setItems();
+ new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().thenAccept(online -> {
+ if (!online) {
+ player.sendMessage(I18n.t("gui_auction.manage.offline_message"));
+ player.closeInventory();
+ return;
+ }
+ setItems();
+ });
}
@Override
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIViewBids.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIViewBids.java
index 641260b3a..f506a2318 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIViewBids.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/GUIViewBids.java
@@ -67,46 +67,47 @@ public void setItems() {
futures.add(future);
});
+ // Render the page once every fetch has resolved — without blocking the caller.
CompletableFuture allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
- allDone.join();
-
- // Sort the items by the time they were added
- auctionItems.sort((o1, o2) -> Long.compare(o2.getEndTime(), o1.getEndTime()));
-
- List auctionItemsPage = auctionItems.getPage(1);
+ allDone.thenRun(() -> {
+ // Sort the items by the time they were added
+ auctionItems.sort((o1, o2) -> Long.compare(o2.getEndTime(), o1.getEndTime()));
+
+ List auctionItemsPage = auctionItems.getPage(1);
+
+ for (int i = 0; i < 7; i++) {
+ int slot = i + 10;
+
+ if (i >= auctionItems.size()) {
+ set(new GUIItem(slot) {
+ @Override
+ public ItemStack.Builder getItem(HypixelPlayer p) {
+ SkyBlockPlayer player = (SkyBlockPlayer) p;
+ return ItemStack.builder(Material.AIR);
+ }
+ });
+ continue;
+ }
- for (int i = 0; i < 7; i++) {
- int slot = i + 10;
+ AuctionItem item = auctionItemsPage.get(i);
+ set(new GUIClickableItem(slot) {
+ @Override
+ public void run(InventoryPreClickEvent e, HypixelPlayer p) {
+ SkyBlockPlayer player = (SkyBlockPlayer) p;
+ new GUIAuctionViewItem(item.getUuid(), GUIViewBids.this).open(player);
+ }
- if (i >= auctionItems.size()) {
- set(new GUIItem(slot) {
@Override
public ItemStack.Builder getItem(HypixelPlayer p) {
SkyBlockPlayer player = (SkyBlockPlayer) p;
- return ItemStack.builder(Material.AIR);
+ return ItemStackCreator.getStack(
+ StringUtility.getTextFromComponent(new NonPlayerItemUpdater(item.getItem()).getUpdatedItem().build()
+ .get(DataComponents.CUSTOM_NAME)),
+ item.getItem().material(), item.getItem().amount(), new AuctionItemLoreHandler(item).getLore(player));
}
});
- continue;
}
-
- AuctionItem item = auctionItemsPage.get(i);
- set(new GUIClickableItem(slot) {
- @Override
- public void run(InventoryPreClickEvent e, HypixelPlayer p) {
- SkyBlockPlayer player = (SkyBlockPlayer) p;
- new GUIAuctionViewItem(item.getUuid(), GUIViewBids.this).open(player);
- }
-
- @Override
- public ItemStack.Builder getItem(HypixelPlayer p) {
- SkyBlockPlayer player = (SkyBlockPlayer) p;
- return ItemStackCreator.getStack(
- StringUtility.getTextFromComponent(new NonPlayerItemUpdater(item.getItem()).getUpdatedItem().build()
- .get(DataComponents.CUSTOM_NAME)),
- item.getItem().material(), item.getItem().amount(), new AuctionItemLoreHandler(item).getLore(player));
- }
- });
- }
+ });
}
@Override
@@ -131,13 +132,14 @@ public void onBottomClick(InventoryPreClickEvent e) {
@Override
public void refreshItems(HypixelPlayer player) {
- if (!new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().join()) {
- player.sendMessage(I18n.t("gui_auction.bids.offline_message"));
- player.closeInventory();
- return;
- }
-
- setItems();
+ new ProxyService(ServiceType.AUCTION_HOUSE).isOnline().thenAccept(online -> {
+ if (!online) {
+ player.sendMessage(I18n.t("gui_auction.bids.offline_message"));
+ player.closeInventory();
+ return;
+ }
+ setItems();
+ });
}
@Override
diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/view/AuctionViewThirdBin.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/view/AuctionViewThirdBin.java
index 00df3929e..8e3c2bd3a 100644
--- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/view/AuctionViewThirdBin.java
+++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/gui/inventories/auction/view/AuctionViewThirdBin.java
@@ -94,6 +94,9 @@ public ItemStack.Builder getItem(HypixelPlayer p) {
gui.set(new GUIClickableItem(31) {
@Override
public void run(InventoryPreClickEvent e, HypixelPlayer p) {
+ // Run the purchase off the tick thread; the blocking joins below park a
+ // cheap virtual thread instead of freezing the server.
+ Thread.startVirtualThread(() -> {
SkyBlockPlayer player = (SkyBlockPlayer) p;
Locale l = p.getLocale();
double coins = player.getSkyblockDataHandler().get(net.swofty.type.skyblockgeneric.data.SkyBlockDataHandler.Data.COINS, DatapointDouble.class).getValue();
@@ -156,6 +159,7 @@ public void run(InventoryPreClickEvent e, HypixelPlayer p) {
ClickEvent.runCommand("/ahview " + item.getUuid())
));
}
+ });
}
@Override
diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java
index 7b7da6da2..f2d3482c0 100644
--- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java
+++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java
@@ -60,6 +60,7 @@
import net.swofty.commons.punishment.PunishmentReason;
import net.swofty.commons.punishment.PunishmentTag;
import net.swofty.commons.punishment.PunishmentType;
+import net.swofty.commons.redis.ProxyHeartbeat;
import net.swofty.commons.redis.RedisClient;
import net.swofty.commons.redis.RedisEndpoint;
import net.swofty.commons.redis.RedisMessageHandler;
@@ -265,6 +266,12 @@ public void onProxyInitialization(ProxyInitializeEvent event) {
RedisAPI.generateInstance(ConfigProvider.settings().getRedisUri());
RedisAPI.getInstance().setFilterId("proxy");
RedisClient.identify(RedisEndpoint.proxy());
+
+ // Advertise proxy liveness via a short-lived Redis key. Game servers watch this
+ // instead of polling the proxy with a per-server request/response every second.
+ ProxyHeartbeat.init(ConfigProvider.settings().getRedisUri());
+ server.getScheduler().buildTask(SkyBlockVelocity.getPlugin(), ProxyHeartbeat::beat)
+ .repeat(Duration.ofSeconds(2)).schedule();
loopThroughPackage("net.swofty.velocity.redis.listeners", RedisMessageHandler.class)
.forEach(RedisHandlerRegistry::register);
RedisProtocol, ?>[] fromProxyProtocols = {