diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index 2d6733f67f..a0caebb094 100644
--- a/runelite-client/pom.xml
+++ b/runelite-client/pom.xml
@@ -168,6 +168,17 @@
+
+
+ net.java.dev.jna
+ jna
+ 4.5.1
+
+
+ net.java.dev.jna
+ jna-platform
+ 4.5.1
+
net.runelite
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperConfig.java
index 0559d2059e..cf2a86d0bf 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperConfig.java
@@ -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;
+ }
}
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 4b7d10849d..7242cce3f0 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
@@ -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());
+ }
}
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 555ea3eee7..b74f2c229f 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
@@ -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;
}
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 13e8d69246..f693d3bd42 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
@@ -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()
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 4a35015ec5..c23face9ba 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
@@ -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 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).
*/
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IPHlpAPI.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IPHlpAPI.java
new file mode 100644
index 0000000000..109ea36f77
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IPHlpAPI.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2018, Adam
+ * 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);
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IcmpEchoReply.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IcmpEchoReply.java
new file mode 100644
index 0000000000..0e1cc198a3
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IcmpEchoReply.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018, Adam
+ * 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 getFieldOrder()
+ {
+ return Arrays.asList("address", "status", "roundTripTime", "dataSize", "reserved", "data", "ttl", "tos", "flags", "optionsSize", "optionsData");
+ }
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/Ping.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/Ping.java
new file mode 100644
index 0000000000..fd9a84fa57
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/Ping.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2018, Adam
+ * 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);
+ }
+ }
+}