Merge pull request #5283 from Adam-/ping2

world switcher: add ping
This commit is contained in:
Adam
2018-12-06 20:16:24 -05:00
committed by GitHub
9 changed files with 407 additions and 18 deletions

View File

@@ -168,6 +168,17 @@
</exclusion>
</exclusions>
</dependency>
<!-- net.runelite:discord also has this -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>net.runelite</groupId>

View File

@@ -79,4 +79,15 @@ public interface WorldHopperConfig extends Config
{
return true;
}
@ConfigItem(
keyName = "ping",
name = "Show world ping",
description = "Shows ping to each game world",
position = 4
)
default boolean ping()
{
return true;
}
}

View File

@@ -25,6 +25,7 @@
*/
package net.runelite.client.plugins.worldhopper;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ObjectArrays;
import com.google.inject.Provides;
@@ -37,6 +38,7 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -73,8 +75,10 @@ import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.input.KeyManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.worldhopper.ping.Ping;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.util.ExecutorServiceExceptionLogger;
import net.runelite.client.util.HotkeyListener;
import net.runelite.client.util.Text;
import net.runelite.client.util.WorldUtil;
@@ -92,6 +96,7 @@ import org.apache.commons.lang3.ArrayUtils;
public class WorldHopperPlugin extends Plugin
{
private static final int WORLD_FETCH_TIMER = 10;
private static final int WORLD_PING_TIMER = 10;
private static final int REFRESH_THROTTLE = 60_000; // ms
private static final int TICK_THROTTLE = (int) Duration.ofMinutes(10).toMillis();
@@ -126,6 +131,8 @@ public class WorldHopperPlugin extends Plugin
@Inject
private WorldHopperConfig config;
private final ScheduledExecutorService hopperExecutorService = new ExecutorServiceExceptionLogger(Executors.newSingleThreadScheduledExecutor());
private NavigationButton navButton;
private WorldSwitcherPanel panel;
@@ -137,9 +144,10 @@ public class WorldHopperPlugin extends Plugin
private int favoriteWorld1, favoriteWorld2;
private ScheduledFuture<?> worldResultFuture;
private ScheduledFuture<?> worldResultFuture, pingFuture;
private WorldResult worldResult;
private Instant lastFetch;
private boolean firstRun;
private final HotkeyListener previousKeyListener = new HotkeyListener(() -> config.previousKey())
{
@@ -167,11 +175,11 @@ public class WorldHopperPlugin extends Plugin
@Override
protected void startUp() throws Exception
{
firstRun = true;
keyManager.registerKeyListener(previousKeyListener);
keyManager.registerKeyListener(nextKeyListener);
worldResultFuture = executorService.scheduleAtFixedRate(this::tick, 0, WORLD_FETCH_TIMER, TimeUnit.MINUTES);
panel = new WorldSwitcherPanel(this);
final BufferedImage icon;
@@ -191,11 +199,17 @@ public class WorldHopperPlugin extends Plugin
{
clientToolbar.addNavigation(navButton);
}
worldResultFuture = executorService.scheduleAtFixedRate(this::tick, 0, WORLD_FETCH_TIMER, TimeUnit.MINUTES);
pingFuture = hopperExecutorService.scheduleAtFixedRate(this::pingWorlds, WORLD_PING_TIMER, WORLD_PING_TIMER, TimeUnit.MINUTES);
}
@Override
protected void shutDown() throws Exception
{
pingFuture.cancel(true);
pingFuture = null;
keyManager.unregisterKeyListener(previousKeyListener);
keyManager.unregisterKeyListener(nextKeyListener);
@@ -205,20 +219,37 @@ public class WorldHopperPlugin extends Plugin
lastFetch = null;
clientToolbar.removeNavigation(navButton);
hopperExecutorService.shutdown();
}
@Subscribe
public void onConfigChanged(final ConfigChanged event)
{
if (event.getGroup().equals(WorldHopperConfig.GROUP) && event.getKey().equals("showSidebar"))
if (event.getGroup().equals(WorldHopperConfig.GROUP))
{
if (config.showSidebar())
switch (event.getKey())
{
clientToolbar.addNavigation(navButton);
}
else
{
clientToolbar.removeNavigation(navButton);
case "showSidebar":
if (config.showSidebar())
{
clientToolbar.addNavigation(navButton);
}
else
{
clientToolbar.removeNavigation(navButton);
}
break;
case "ping":
if (config.ping())
{
SwingUtilities.invokeLater(() -> panel.showPing());
}
else
{
SwingUtilities.invokeLater(() -> panel.hidePing());
}
break;
}
}
}
@@ -400,6 +431,13 @@ public class WorldHopperPlugin extends Plugin
}
fetchWorlds();
// Ping worlds once at startup
if (firstRun)
{
firstRun = false;
hopperExecutorService.execute(this::pingWorlds);
}
}
void refresh()
@@ -690,4 +728,24 @@ public class WorldHopperPlugin extends Plugin
return null;
}
private void pingWorlds()
{
if (worldResult == null || !config.showSidebar() || !config.ping())
{
return;
}
Stopwatch stopwatch = Stopwatch.createStarted();
for (World world : worldResult.getWorlds())
{
int ping = Ping.ping(world);
SwingUtilities.invokeLater(() -> panel.updatePing(world, ping));
}
stopwatch.stop();
log.debug("Done pinging worlds in {}", stopwatch.elapsed());
}
}

View File

@@ -35,7 +35,6 @@ 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;
import net.runelite.client.ui.DynamicGridLayout;
@@ -49,12 +48,14 @@ class WorldSwitcherPanel extends PluginPanel
private static final int WORLD_COLUMN_WIDTH = 60;
private static final int PLAYERS_COLUMN_WIDTH = 40;
private static final int PING_COLUMN_WIDTH = 47;
private final JPanel listContainer = new JPanel();
private WorldTableHeader worldHeader;
private WorldTableHeader playersHeader;
private WorldTableHeader activityHeader;
private WorldTableHeader pingHeader;
private WorldOrder orderIndex = WorldOrder.WORLD;
private boolean ascendingOrder = true;
@@ -112,12 +113,48 @@ class WorldSwitcherPanel extends PluginPanel
}
}
void updatePing(World world, int ping)
{
for (WorldTableRow worldTableRow : rows)
{
if (worldTableRow.getWorld() == world)
{
worldTableRow.setPing(ping);
// If the panel is sorted by ping, re-sort it
if (orderIndex == WorldOrder.PING)
{
updateList();
}
break;
}
}
}
void hidePing()
{
for (WorldTableRow worldTableRow : rows)
{
worldTableRow.hidePing();
}
}
void showPing()
{
for (WorldTableRow worldTableRow : rows)
{
worldTableRow.showPing();
}
}
void updateList()
{
rows.sort((r1, r2) ->
{
switch (orderIndex)
{
case PING:
return Integer.compare(r1.getPing(), r2.getPing()) * (ascendingOrder ? 1 : -1);
case WORLD:
return Integer.compare(r1.getWorld().getId(), r2.getWorld().getId()) * (ascendingOrder ? 1 : -1);
case PLAYERS:
@@ -126,7 +163,6 @@ class WorldSwitcherPanel extends PluginPanel
return r1.getWorld().getActivity().compareTo(r2.getWorld().getActivity()) * -1 * (ascendingOrder ? 1 : -1);
default:
return 0;
}
});
@@ -191,12 +227,16 @@ class WorldSwitcherPanel extends PluginPanel
private void orderBy(WorldOrder order)
{
pingHeader.highlight(false, ascendingOrder);
worldHeader.highlight(false, ascendingOrder);
playersHeader.highlight(false, ascendingOrder);
activityHeader.highlight(false, ascendingOrder);
switch (order)
{
case PING:
pingHeader.highlight(true, ascendingOrder);
break;
case WORLD:
worldHeader.highlight(true, ascendingOrder);
break;
@@ -219,6 +259,23 @@ class WorldSwitcherPanel extends PluginPanel
{
JPanel header = new JPanel(new BorderLayout());
JPanel leftSide = new JPanel(new BorderLayout());
JPanel rightSide = new JPanel(new BorderLayout());
pingHeader = new WorldTableHeader("Ping", orderIndex == WorldOrder.PING, ascendingOrder, plugin::refresh);
pingHeader.setPreferredSize(new Dimension(PING_COLUMN_WIDTH, 0));
pingHeader.addMouseListener(new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent mouseEvent)
{
if (SwingUtilities.isRightMouseButton(mouseEvent))
{
return;
}
ascendingOrder = orderIndex != WorldOrder.PING || !ascendingOrder;
orderBy(WorldOrder.PING);
}
});
worldHeader = new WorldTableHeader("World", orderIndex == WorldOrder.WORLD, ascendingOrder, plugin::refresh);
worldHeader.setPreferredSize(new Dimension(WORLD_COLUMN_WIDTH, 0));
@@ -253,7 +310,6 @@ class WorldSwitcherPanel extends PluginPanel
});
activityHeader = new WorldTableHeader("Activity", orderIndex == WorldOrder.ACTIVITY, ascendingOrder, plugin::refresh);
activityHeader.setBorder(new EmptyBorder(3, 5, 3, 5));
activityHeader.addMouseListener(new MouseAdapter()
{
@Override
@@ -269,10 +325,13 @@ class WorldSwitcherPanel extends PluginPanel
});
leftSide.add(worldHeader, BorderLayout.WEST);
leftSide.add(playersHeader, BorderLayout.EAST);
leftSide.add(playersHeader, BorderLayout.CENTER);
rightSide.add(activityHeader, BorderLayout.CENTER);
rightSide.add(pingHeader, BorderLayout.EAST);
header.add(leftSide, BorderLayout.WEST);
header.add(activityHeader, BorderLayout.CENTER);
header.add(rightSide, BorderLayout.CENTER);
return header;
}

View File

@@ -69,7 +69,7 @@ class WorldTableHeader extends JPanel
setLayout(new BorderLayout(5, 0));
setBorder(new CompoundBorder(
BorderFactory.createMatteBorder(0, 0, 0, 1, ColorScheme.MEDIUM_GRAY_COLOR),
new EmptyBorder(0, 5, 0, 5)));
new EmptyBorder(0, 5, 0, 2)));
setBackground(ColorScheme.SCROLL_TRACK_COLOR);
addMouseListener(new MouseAdapter()

View File

@@ -54,6 +54,7 @@ class WorldTableRow extends JPanel
private static final int WORLD_COLUMN_WIDTH = 60;
private static final int PLAYERS_COLUMN_WIDTH = 40;
private static final int PING_COLUMN_WIDTH = 35;
private static final Color CURRENT_WORLD = new Color(66, 227, 17);
private static final Color UNAVAILABLE_WORLD = Color.GRAY.darker().darker();
@@ -75,6 +76,7 @@ class WorldTableRow extends JPanel
private JLabel worldField;
private JLabel playerCountField;
private JLabel activityField;
private JLabel pingField;
private BiConsumer<World, Boolean> onFavorite;
@Getter
@@ -83,6 +85,8 @@ class WorldTableRow extends JPanel
@Getter(AccessLevel.PACKAGE)
private int updatedPlayerCount;
private int ping;
private Color lastBackground;
private boolean current;
@@ -151,12 +155,18 @@ class WorldTableRow extends JPanel
setComponentPopupMenu(popupMenu);
JPanel leftSide = new JPanel(new BorderLayout());
JPanel rightSide = new JPanel(new BorderLayout());
leftSide.setOpaque(false);
rightSide.setOpaque(false);
JPanel worldField = buildWorldField();
worldField.setPreferredSize(new Dimension(WORLD_COLUMN_WIDTH, 0));
worldField.setOpaque(false);
JPanel pingField = buildPingField();
pingField.setPreferredSize(new Dimension(PING_COLUMN_WIDTH, 0));
pingField.setOpaque(false);
JPanel playersField = buildPlayersField();
playersField.setPreferredSize(new Dimension(PLAYERS_COLUMN_WIDTH, 0));
playersField.setOpaque(false);
@@ -168,10 +178,12 @@ class WorldTableRow extends JPanel
recolour(current);
leftSide.add(worldField, BorderLayout.WEST);
leftSide.add(playersField, BorderLayout.EAST);
leftSide.add(playersField, BorderLayout.CENTER);
rightSide.add(activityField, BorderLayout.CENTER);
rightSide.add(pingField, BorderLayout.EAST);
add(leftSide, BorderLayout.WEST);
add(activityField, BorderLayout.CENTER);
add(rightSide, BorderLayout.CENTER);
}
void setFavoriteMenu(boolean favorite)
@@ -199,9 +211,31 @@ class WorldTableRow extends JPanel
playerCountField.setText(String.valueOf(playerCount));
}
void setPing(int ping)
{
this.ping = ping;
pingField.setText(ping <= 0 ? "-" : Integer.toString(ping));
}
void hidePing()
{
pingField.setText("-");
}
void showPing()
{
setPing(ping); // to update pingField
}
int getPing()
{
return ping;
}
public void recolour(boolean current)
{
playerCountField.setForeground(current ? CURRENT_WORLD : Color.WHITE);
pingField.setForeground(current ? CURRENT_WORLD : Color.WHITE);
if (current)
{
@@ -244,6 +278,19 @@ class WorldTableRow extends JPanel
return column;
}
private JPanel buildPingField()
{
JPanel column = new JPanel(new BorderLayout());
column.setBorder(new EmptyBorder(0, 5, 0, 5));
pingField = new JLabel("-");
pingField.setFont(FontManager.getRunescapeSmallFont());
column.add(pingField, BorderLayout.EAST);
return column;
}
/**
* Builds the activity list field (containing that world's activity/theme).
*/

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.worldhopper.ping;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
interface IPHlpAPI extends Library
{
IPHlpAPI INSTANCE = Native.loadLibrary("IPHlpAPI", IPHlpAPI.class);
Pointer IcmpCreateFile();
boolean IcmpCloseHandle(Pointer handle);
int IcmpSendEcho(Pointer IcmpHandle, int DestinationAddress, Pointer RequestData, short RequestSize, Pointer RequestOptions, IcmpEchoReply ReplyBuffer, int ReplySize, int Timeout);
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.worldhopper.ping;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.WinDef;
import java.util.Arrays;
import java.util.List;
public class IcmpEchoReply extends Structure
{
private static final int IP_OPTION_INFO_SIZE = 1 + 1 + 1 + 1 + (Pointer.SIZE == 8 ? 12 : 4); // on 64bit vms add 4 byte padding
public static final int SIZE = 4 + 4 + 4 + 2 + 2 + Pointer.SIZE + IP_OPTION_INFO_SIZE;
public WinDef.ULONG address;
public WinDef.ULONG status;
public WinDef.ULONG roundTripTime;
public WinDef.USHORT dataSize;
public WinDef.USHORT reserved;
public WinDef.PVOID data;
public WinDef.UCHAR ttl;
public WinDef.UCHAR tos;
public WinDef.UCHAR flags;
public WinDef.UCHAR optionsSize;
public WinDef.PVOID optionsData;
IcmpEchoReply(Pointer p)
{
super(p);
}
@Override
protected List<String> getFieldOrder()
{
return Arrays.asList("address", "status", "roundTripTime", "dataSize", "reserved", "data", "ttl", "tos", "flags", "optionsSize", "optionsData");
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.worldhopper.ping;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.util.OSType;
import net.runelite.http.api.worlds.World;
@Slf4j
public class Ping
{
private static final String RUNELITE_PING = "RuneLitePing";
private static final int TIMEOUT = 2000;
private static final int PORT = 43594;
public static int ping(World world)
{
try
{
switch (OSType.getOSType())
{
case Windows:
return windowsPing(world);
default:
return tcpPing(world);
}
}
catch (IOException ex)
{
log.warn("error pinging", ex);
return -1;
}
}
private static int windowsPing(World world) throws UnknownHostException
{
IPHlpAPI ipHlpAPI = IPHlpAPI.INSTANCE;
Pointer ptr = ipHlpAPI.IcmpCreateFile();
InetAddress inetAddress = InetAddress.getByName(world.getAddress());
byte[] address = inetAddress.getAddress();
String dataStr = RUNELITE_PING;
int dataLength = dataStr.length() + 1;
Pointer data = new Memory(dataLength);
data.setString(0L, dataStr);
IcmpEchoReply icmpEchoReply = new IcmpEchoReply(new Memory(IcmpEchoReply.SIZE + dataLength));
assert icmpEchoReply.size() == IcmpEchoReply.SIZE;
int packed = (address[0] & 0xff) | ((address[1] & 0xff) << 8) | ((address[2] & 0xff) << 16) | ((address[3] & 0xff) << 24);
int ret = ipHlpAPI.IcmpSendEcho(ptr, packed, data, (short) (dataLength), Pointer.NULL, icmpEchoReply, IcmpEchoReply.SIZE + dataLength, TIMEOUT);
if (ret != 1)
{
ipHlpAPI.IcmpCloseHandle(ptr);
return -1;
}
int rtt = Math.toIntExact(icmpEchoReply.roundTripTime.longValue());
ipHlpAPI.IcmpCloseHandle(ptr);
return rtt;
}
private static int tcpPing(World world) throws IOException
{
try (Socket socket = new Socket())
{
socket.setSoTimeout(TIMEOUT);
InetAddress inetAddress = InetAddress.getByName(world.getAddress());
long start = System.nanoTime();
socket.connect(new InetSocketAddress(inetAddress, PORT));
long end = System.nanoTime();
return (int) ((end - start) / 1000000L);
}
}
}