From b0a9d249282c0c05cb703874e8907a569c1fd4ae Mon Sep 17 00:00:00 2001 From: Ruben Amendoeira Date: Thu, 16 Aug 2018 00:59:30 +0100 Subject: [PATCH] World hopper data refreshing (#4918) Update world list on world hop. Schedule a refresh every 10 minutes, and add a refresh option. --- .../worldhopper/WorldHopperPlugin.java | 129 +++++++++++++++--- .../worldhopper/WorldSwitcherPanel.java | 92 ++++++++++--- .../plugins/worldhopper/WorldTableHeader.java | 18 ++- .../plugins/worldhopper/WorldTableRow.java | 101 ++++++++------ 4 files changed, 260 insertions(+), 80 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperPlugin.java index 1db1874b26..b734ed491a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperPlugin.java @@ -31,11 +31,16 @@ import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; import java.awt.image.BufferedImage; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.Comparator; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; -import java.util.concurrent.Future; +import java.util.Map; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import javax.inject.Inject; import javax.swing.SwingUtilities; @@ -50,9 +55,11 @@ import net.runelite.api.MenuAction; import net.runelite.api.MenuEntry; import net.runelite.api.Varbits; import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.events.PlayerMenuOptionClicked; import net.runelite.api.events.VarbitChanged; +import net.runelite.api.events.WorldListLoad; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.callback.ClientThread; import net.runelite.client.chat.ChatColorType; @@ -81,6 +88,10 @@ import org.apache.commons.lang3.ArrayUtils; @Slf4j public class WorldHopperPlugin extends Plugin { + private static final int WORLD_FETCH_TIMER = 10; + private static final int REFRESH_THROTTLE = 60_000; // ms + private static final int TICK_THROTTLE = (int) Duration.ofMinutes(10).toMillis(); + private static final String HOP_TO = "Hop-to"; private static final String KICK_OPTION = "Kick"; private static final ImmutableList BEFORE_OPTIONS = ImmutableList.of("Add friend", "Remove friend", KICK_OPTION); @@ -113,9 +124,14 @@ public class WorldHopperPlugin extends Plugin private NavigationButton navButton; private WorldSwitcherPanel panel; + private int lastWorld; + private int favoriteWorld1, favoriteWorld2; - private Future worldResultFuture; + + private ScheduledFuture worldResultFuture; private WorldResult worldResult; + private Instant lastFetch; + private final HotkeyListener previousKeyListener = new HotkeyListener(() -> config.previousKey()) { @Override @@ -145,25 +161,7 @@ public class WorldHopperPlugin extends Plugin keyManager.registerKeyListener(previousKeyListener); keyManager.registerKeyListener(nextKeyListener); - worldResultFuture = executorService.submit(() -> - { - try - { - WorldResult worldResult = new WorldClient().lookupWorlds(); - - if (worldResult != null) - { - worldResult.getWorlds().sort(Comparator.comparingInt(World::getId)); - this.worldResult = worldResult; - - SwingUtilities.invokeLater(() -> panel.populate(worldResult.getWorlds())); - } - } - catch (IOException ex) - { - log.warn("Error looking up worlds", ex); - } - }); + worldResultFuture = executorService.scheduleAtFixedRate(this::tick, 0, WORLD_FETCH_TIMER, TimeUnit.MINUTES); panel = new WorldSwitcherPanel(this); @@ -195,6 +193,7 @@ public class WorldHopperPlugin extends Plugin worldResultFuture.cancel(true); worldResultFuture = null; worldResult = null; + lastFetch = null; clientToolbar.removeNavigation(navButton); } @@ -347,6 +346,94 @@ public class WorldHopperPlugin extends Plugin } } + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) + { + // If the player has disabled the side bar plugin panel, do not update the UI + if (config.showSidebar() && gameStateChanged.getGameState() == GameState.LOGGED_IN) + { + if (lastWorld != client.getWorld()) + { + int newWorld = client.getWorld(); + panel.switchCurrentHighlight(newWorld, lastWorld); + lastWorld = newWorld; + } + } + } + + @Subscribe + public void onWorldListLoad(WorldListLoad worldListLoad) + { + if (!config.showSidebar()) + { + return; + } + + Map worldData = new HashMap<>(); + + for (net.runelite.api.World w : worldListLoad.getWorlds()) + { + worldData.put(w.getId(), w.getPlayerCount()); + } + + panel.updateListData(worldData); + this.lastFetch = Instant.now(); // This counts as a fetch as it updates populations + } + + private void tick() + { + Instant now = Instant.now(); + if (lastFetch != null && now.toEpochMilli() - lastFetch.toEpochMilli() < TICK_THROTTLE) + { + log.debug("Throttling world refresh tick"); + return; + } + + fetchWorlds(); + } + + void refresh() + { + Instant now = Instant.now(); + if (lastFetch != null && now.toEpochMilli() - lastFetch.toEpochMilli() < REFRESH_THROTTLE) + { + log.debug("Throttling world refresh"); + return; + } + + fetchWorlds(); + } + + private void fetchWorlds() + { + log.debug("Fetching worlds"); + + try + { + WorldResult worldResult = new WorldClient().lookupWorlds(); + + if (worldResult != null) + { + worldResult.getWorlds().sort(Comparator.comparingInt(World::getId)); + this.worldResult = worldResult; + this.lastFetch = Instant.now(); + updateList(); + } + } + catch (IOException ex) + { + log.warn("Error looking up worlds", ex); + } + } + + /** + * This method ONLY updates the list's UI, not the actual world list and data it displays. + */ + private void updateList() + { + SwingUtilities.invokeLater(() -> panel.populate(worldResult.getWorlds())); + } + private void hop(boolean previous) { if (worldResult == null || client.getGameState() != GameState.LOGGED_IN) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldSwitcherPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldSwitcherPanel.java index befd77ffb1..537e2bf375 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldSwitcherPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldSwitcherPanel.java @@ -32,7 +32,9 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; +import java.util.Map; import javax.swing.JPanel; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import lombok.extern.slf4j.Slf4j; import net.runelite.client.ui.ColorScheme; @@ -57,7 +59,7 @@ class WorldSwitcherPanel extends PluginPanel private WorldOrder orderIndex = WorldOrder.WORLD; private boolean ascendingOrder = true; - private List worlds; + private ArrayList rows = new ArrayList<>(); private WorldHopperPlugin plugin; WorldSwitcherPanel(WorldHopperPlugin plugin) @@ -75,37 +77,73 @@ class WorldSwitcherPanel extends PluginPanel add(listContainer); } + void switchCurrentHighlight(int newWorld, int lastWorld) + { + for (WorldTableRow row : rows) + { + if (row.getWorld().getId() == newWorld) + { + row.recolour(true); + } + else if (row.getWorld().getId() == lastWorld) + { + row.recolour(false); + } + } + } + + void updateListData(Map worldData) + { + for (WorldTableRow worldTableRow : rows) + { + World world = worldTableRow.getWorld(); + Integer playerCount = worldData.get(world.getId()); + if (playerCount != null) + { + worldTableRow.updatePlayerCount(playerCount); + } + } + + // If the list is being ordered by player count, then it has to be re-painted + // to properly display the new data + if (orderIndex == WorldOrder.PLAYERS) + { + updateList(); + } + } + void updateList() { - worlds.sort((w1, w2) -> + rows.sort((r1, r2) -> { switch (orderIndex) { case WORLD: - return Integer.compare(w1.getId(), w2.getId()) * (ascendingOrder ? 1 : -1); + return Integer.compare(r1.getWorld().getId(), r2.getWorld().getId()) * (ascendingOrder ? 1 : -1); case PLAYERS: - return Integer.compare(w1.getPlayers(), w2.getPlayers()) * (ascendingOrder ? 1 : -1); + return Integer.compare(r1.getUpdatedPlayerCount(), r2.getUpdatedPlayerCount()) * (ascendingOrder ? 1 : -1); case ACTIVITY: - return w1.getActivity().compareTo(w2.getActivity()) * (ascendingOrder ? 1 : -1); + return r1.getWorld().getActivity().compareTo(r2.getWorld().getActivity()) * (ascendingOrder ? 1 : -1); default: return 0; } }); - worlds.sort((w1, w2) -> + rows.sort((r1, r2) -> { - boolean b1 = plugin.isFavorite(w1); - boolean b2 = plugin.isFavorite(w2); + boolean b1 = plugin.isFavorite(r1.getWorld()); + boolean b2 = plugin.isFavorite(r2.getWorld()); return Boolean.compare(b2, b1); }); listContainer.removeAll(); - for (int i = 0; i < worlds.size(); i++) + for (int i = 0; i < rows.size(); i++) { - World world = worlds.get(i); - listContainer.add(buildRow(world, i % 2 == 0, world.getId() == plugin.getCurrentWorld(), plugin.isFavorite(world))); + WorldTableRow row = rows.get(i); + row.setBackground(i % 2 == 0 ? ODD_ROW : ColorScheme.DARK_GRAY_COLOR); + listContainer.add(row); } listContainer.revalidate(); @@ -114,7 +152,14 @@ class WorldSwitcherPanel extends PluginPanel void populate(List worlds) { - this.worlds = new ArrayList<>(worlds); + rows.clear(); + + for (int i = 0; i < worlds.size(); i++) + { + World world = worlds.get(i); + rows.add(buildRow(world, i % 2 == 0, world.getId() == plugin.getCurrentWorld(), plugin.isFavorite(world))); + } + updateList(); } @@ -149,38 +194,49 @@ class WorldSwitcherPanel extends PluginPanel JPanel header = new JPanel(new BorderLayout()); JPanel leftSide = new JPanel(new BorderLayout()); - worldHeader = new WorldTableHeader("World", orderIndex == WorldOrder.WORLD, ascendingOrder); + worldHeader = new WorldTableHeader("World", orderIndex == WorldOrder.WORLD, ascendingOrder, plugin::refresh); worldHeader.setPreferredSize(new Dimension(WORLD_COLUMN_WIDTH, 0)); worldHeader.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent mouseEvent) { + if (SwingUtilities.isRightMouseButton(mouseEvent)) + { + return; + } ascendingOrder = orderIndex != WorldOrder.WORLD || !ascendingOrder; orderBy(WorldOrder.WORLD); } }); - playersHeader = new WorldTableHeader("#", orderIndex == WorldOrder.PLAYERS, ascendingOrder); + playersHeader = new WorldTableHeader("#", orderIndex == WorldOrder.PLAYERS, ascendingOrder, plugin::refresh); playersHeader.setPreferredSize(new Dimension(PLAYERS_COLUMN_WIDTH, 0)); playersHeader.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent mouseEvent) { - + if (SwingUtilities.isRightMouseButton(mouseEvent)) + { + return; + } ascendingOrder = orderIndex != WorldOrder.PLAYERS || !ascendingOrder; orderBy(WorldOrder.PLAYERS); } }); - activityHeader = new WorldTableHeader("Activity", orderIndex == WorldOrder.ACTIVITY, ascendingOrder); + activityHeader = new WorldTableHeader("Activity", orderIndex == WorldOrder.ACTIVITY, ascendingOrder, plugin::refresh); activityHeader.setBorder(new EmptyBorder(3, 5, 3, 5)); activityHeader.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent mouseEvent) { + if (SwingUtilities.isRightMouseButton(mouseEvent)) + { + return; + } ascendingOrder = orderIndex != WorldOrder.ACTIVITY || !ascendingOrder; orderBy(WorldOrder.ACTIVITY); } @@ -198,9 +254,9 @@ class WorldSwitcherPanel extends PluginPanel /** * Builds a table row, that displays the world's information. */ - private JPanel buildRow(World world, boolean stripe, boolean current, boolean favorite) + private WorldTableRow buildRow(World world, boolean stripe, boolean current, boolean favorite) { - JPanel row = new WorldTableRow(world, current, favorite, + WorldTableRow row = new WorldTableRow(world, current, favorite, world1 -> { plugin.hopTo(world1); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java index d820fcd44a..13e8d69246 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java @@ -30,10 +30,13 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; +import javax.annotation.Nonnull; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JLabel; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import net.runelite.client.ui.ColorScheme; @@ -61,7 +64,7 @@ class WorldTableHeader extends JPanel // Determines if this header column is being used to order the list private boolean ordering = false; - WorldTableHeader(String title, boolean ordered, boolean ascending) + WorldTableHeader(String title, boolean ordered, boolean ascending, @Nonnull Runnable onRefresh) { setLayout(new BorderLayout(5, 0)); setBorder(new CompoundBorder( @@ -93,6 +96,19 @@ class WorldTableHeader extends JPanel textLabel.setText(title); textLabel.setFont(FontManager.getRunescapeSmallFont()); + final JMenuItem refresh = new JMenuItem("Refresh worlds"); + refresh.addActionListener(e -> + { + onRefresh.run(); + }); + + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(refresh); + + textLabel.setComponentPopupMenu(popupMenu); + setComponentPopupMenu(popupMenu); + highlight(ordered, ascending); add(textLabel, BorderLayout.WEST); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableRow.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableRow.java index a61aa21aaf..e0182cb663 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableRow.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableRow.java @@ -37,6 +37,8 @@ import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.border.EmptyBorder; +import lombok.AccessLevel; +import lombok.Getter; import net.runelite.client.ui.FontManager; import net.runelite.client.util.ImageUtil; import net.runelite.http.api.worlds.World; @@ -67,12 +69,24 @@ class WorldTableRow extends JPanel FLAG_GER = new ImageIcon(ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "flag_ger.png")); } + private JLabel worldField; + private JLabel playerCountField; + private JLabel activityField; + + @Getter + private final World world; + + @Getter(AccessLevel.PACKAGE) + private int updatedPlayerCount; + private Color lastBackground; private boolean current; WorldTableRow(World world, boolean current, boolean favorite, Consumer onSelect, BiConsumer onFavorite) { this.current = current; + this.world = world; + this.updatedPlayerCount = world.getPlayers(); setLayout(new BorderLayout()); setBorder(new EmptyBorder(2, 0, 2, 0)); @@ -142,18 +156,20 @@ class WorldTableRow extends JPanel JPanel leftSide = new JPanel(new BorderLayout()); leftSide.setOpaque(false); - JPanel worldField = buildWorldField(world); + JPanel worldField = buildWorldField(); worldField.setPreferredSize(new Dimension(WORLD_COLUMN_WIDTH, 0)); worldField.setOpaque(false); - JPanel playersField = buildPlayersField(world); + JPanel playersField = buildPlayersField(); playersField.setPreferredSize(new Dimension(PLAYERS_COLUMN_WIDTH, 0)); playersField.setOpaque(false); - JPanel activityField = buildActivityField(world); + JPanel activityField = buildActivityField(); activityField.setBorder(new EmptyBorder(5, 5, 5, 5)); activityField.setOpaque(false); + recolour(current); + leftSide.add(worldField, BorderLayout.WEST); leftSide.add(playersField, BorderLayout.EAST); @@ -161,19 +177,49 @@ class WorldTableRow extends JPanel add(activityField, BorderLayout.CENTER); } + void updatePlayerCount(int playerCount) + { + this.updatedPlayerCount = playerCount; + playerCountField.setText(String.valueOf(playerCount)); + } + + public void recolour(boolean current) + { + playerCountField.setForeground(current ? CURRENT_WORLD : Color.WHITE); + + if (current) + { + activityField.setForeground(CURRENT_WORLD); + worldField.setForeground(CURRENT_WORLD); + return; + } + else if (world.getTypes().contains(WorldType.PVP) + || world.getTypes().contains(WorldType.PVP_HIGH_RISK) + || world.getTypes().contains(WorldType.DEADMAN) + || world.getTypes().contains(WorldType.SEASONAL_DEADMAN)) + { + activityField.setForeground(DANGEROUS_WORLD); + } + else if (world.getTypes().contains(WorldType.TOURNAMENT)) + { + activityField.setForeground(TOURNAMENT_WORLD); + } + + worldField.setForeground(world.getTypes().contains(WorldType.MEMBERS) ? MEMBERS_WORLD : FREE_WORLD); + } + /** * Builds the players list field (containing the amount of players logged in that world). */ - private JPanel buildPlayersField(World world) + private JPanel buildPlayersField() { JPanel column = new JPanel(new BorderLayout()); column.setBorder(new EmptyBorder(0, 5, 0, 5)); - JLabel label = new JLabel(world.getPlayers() + ""); - label.setFont(FontManager.getRunescapeSmallFont()); - label.setForeground(current ? CURRENT_WORLD : Color.WHITE); + playerCountField = new JLabel(world.getPlayers() + ""); + playerCountField.setFont(FontManager.getRunescapeSmallFont()); - column.add(label, BorderLayout.WEST); + column.add(playerCountField, BorderLayout.WEST); return column; } @@ -181,31 +227,15 @@ class WorldTableRow extends JPanel /** * Builds the activity list field (containing that world's activity/theme). */ - private JPanel buildActivityField(World world) + private JPanel buildActivityField() { JPanel column = new JPanel(new BorderLayout()); column.setBorder(new EmptyBorder(0, 5, 0, 5)); - JLabel label = new JLabel(world.getActivity()); - label.setFont(FontManager.getRunescapeSmallFont()); + activityField = new JLabel(world.getActivity()); + activityField.setFont(FontManager.getRunescapeSmallFont()); - if (current) - { - label.setForeground(CURRENT_WORLD); - } - else if (world.getTypes().contains(WorldType.PVP) - || world.getTypes().contains(WorldType.PVP_HIGH_RISK) - || world.getTypes().contains(WorldType.DEADMAN) - || world.getTypes().contains(WorldType.SEASONAL_DEADMAN)) - { - label.setForeground(DANGEROUS_WORLD); - } - else if (world.getTypes().contains(WorldType.TOURNAMENT)) - { - label.setForeground(TOURNAMENT_WORLD); - } - - column.add(label, BorderLayout.WEST); + column.add(activityField, BorderLayout.WEST); return column; } @@ -213,26 +243,17 @@ class WorldTableRow extends JPanel /** * Builds the world list field (containing the country's flag and the world index). */ - private JPanel buildWorldField(World world) + private JPanel buildWorldField() { JPanel column = new JPanel(new BorderLayout(7, 0)); column.setBorder(new EmptyBorder(0, 5, 0, 5)); - JLabel label = new JLabel(world.getId() + ""); - - if (current) - { - label.setForeground(CURRENT_WORLD); - } - else - { - label.setForeground(world.getTypes().contains(WorldType.MEMBERS) ? MEMBERS_WORLD : FREE_WORLD); - } + worldField = new JLabel(world.getId() + ""); JLabel flag = new JLabel(getFlag(world.getLocation())); column.add(flag, BorderLayout.WEST); - column.add(label, BorderLayout.CENTER); + column.add(worldField, BorderLayout.CENTER); return column; }