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 = {