From 8e70ad5cc9f2976f682504c818bb1ab69e9e4717 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 4 Dec 2018 19:13:49 -0500 Subject: [PATCH] world hopper: add world ping Co-authored-by: Richard Brown richardjbrown1@gmail.com --- runelite-client/pom.xml | 11 ++ .../worldhopper/WorldHopperConfig.java | 11 ++ .../worldhopper/WorldHopperPlugin.java | 78 +++++++++++-- .../worldhopper/WorldSwitcherPanel.java | 69 +++++++++++- .../plugins/worldhopper/WorldTableHeader.java | 2 +- .../plugins/worldhopper/WorldTableRow.java | 51 ++++++++- .../plugins/worldhopper/ping/IPHlpAPI.java | 40 +++++++ .../worldhopper/ping/IcmpEchoReply.java | 60 ++++++++++ .../client/plugins/worldhopper/ping/Ping.java | 103 ++++++++++++++++++ 9 files changed, 407 insertions(+), 18 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IPHlpAPI.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/IcmpEchoReply.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/ping/Ping.java 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); + } + } +}