From 12da232985a863e2dd3b5f1689fcb11d62559304 Mon Sep 17 00:00:00 2001 From: Lotto Date: Tue, 7 Aug 2018 19:53:29 +0100 Subject: [PATCH] runelite-client: add world hopper plugin Co-authored-by: Adam Co-authored-by: psikoi Co-authored-by: Tomas Slusny --- .../main/java/net/runelite/api/Client.java | 9 +- .../main/java/net/runelite/api/Varbits.java | 5 +- .../runelite/client/config/ConfigManager.java | 2 +- .../defaultworld/DefaultWorldPlugin.java | 17 +- .../worldhopper/WorldHopperConfig.java | 82 +++ .../worldhopper/WorldHopperPlugin.java | 543 ++++++++++++++++++ .../worldhopper/WorldSwitcherPanel.java | 236 ++++++++ .../plugins/worldhopper/WorldTableHeader.java | 123 ++++ .../plugins/worldhopper/WorldTableRow.java | 254 ++++++++ .../net/runelite/client/util/WorldUtil.java | 52 ++ .../client/plugins/worldhopper/arrow_down.png | Bin 0 -> 89 bytes .../client/plugins/worldhopper/flag_aus.png | Bin 0 -> 534 bytes .../client/plugins/worldhopper/flag_ger.png | Bin 0 -> 433 bytes .../client/plugins/worldhopper/flag_uk.png | Bin 0 -> 366 bytes .../client/plugins/worldhopper/flag_us.png | Bin 0 -> 589 bytes .../client/plugins/worldhopper/icon.png | Bin 0 -> 859 bytes .../runelite/mixins/WorldHoppingMixin.java | 6 +- 17 files changed, 1309 insertions(+), 20 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldSwitcherPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableRow.java create mode 100644 runelite-client/src/main/java/net/runelite/client/util/WorldUtil.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/arrow_down.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_aus.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_ger.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_uk.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_us.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/icon.png diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java index 4c82b13fab..b330c4629a 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -1434,7 +1434,14 @@ public interface Client extends GameEngine */ void setOculusOrbNormalSpeed(int speed); + /** + * Opens in-game world hopper interface + */ void openWorldHopper(); - void hopToWorld(int world); + /** + * Hops using in-game world hopper widget to another world + * @param world target world to hop to + */ + void hopToWorld(World world); } diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java index 3e452844c1..b513c5caa6 100644 --- a/runelite-api/src/main/java/net/runelite/api/Varbits.java +++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java @@ -382,7 +382,10 @@ public enum Varbits /** * Corp beast damage */ - CORP_DAMAGE(999); + CORP_DAMAGE(999), + + WORLDHOPPER_FAVROITE_1(4597), + WORLDHOPPER_FAVROITE_2(4598); /** * The raw varbit ID. diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 64667746a3..8affebbea6 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -431,7 +431,7 @@ public class ConfigManager static Object stringToObject(String str, Class type) { - if (type == boolean.class) + if (type == boolean.class || type == Boolean.class) { return Boolean.parseBoolean(str); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/defaultworld/DefaultWorldPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/defaultworld/DefaultWorldPlugin.java index 078444bb16..96513a60a9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/defaultworld/DefaultWorldPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/defaultworld/DefaultWorldPlugin.java @@ -27,17 +27,16 @@ package net.runelite.client.plugins.defaultworld; import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; import java.io.IOException; -import java.util.EnumSet; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.GameState; -import net.runelite.api.WorldType; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.SessionOpen; import net.runelite.client.config.ConfigManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.WorldUtil; import net.runelite.http.api.worlds.World; import net.runelite.http.api.worlds.WorldClient; import net.runelite.http.api.worlds.WorldResult; @@ -122,7 +121,7 @@ public class DefaultWorldPlugin extends Plugin rsWorld.setId(world.getId()); rsWorld.setPlayerCount(world.getPlayers()); rsWorld.setLocation(world.getLocation()); - rsWorld.setTypes(toWorldTypes(world.getTypes())); + rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); client.changeWorld(rsWorld); log.debug("Applied new world {}", correctedWorld); @@ -138,18 +137,6 @@ public class DefaultWorldPlugin extends Plugin } } - private static EnumSet toWorldTypes(final EnumSet apiTypes) - { - final EnumSet types = EnumSet.noneOf(WorldType.class); - - for (net.runelite.http.api.worlds.WorldType apiType : apiTypes) - { - types.add(WorldType.valueOf(apiType.name())); - } - - return types; - } - private void applyWorld() { if (worldCache == 0) 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 new file mode 100644 index 0000000000..0559d2059e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperConfig.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018, Lotto + * 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; + +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Keybind; + +@ConfigGroup(WorldHopperConfig.GROUP) +public interface WorldHopperConfig extends Config +{ + String GROUP = "worldhopper"; + + @ConfigItem( + keyName = "previousKey", + name = "Quick-hop previous", + description = "When you press this key you'll hop to the previous world", + position = 0 + ) + default Keybind previousKey() + { + return new Keybind(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK); + } + + @ConfigItem( + keyName = "nextKey", + name = "Quick-hop next", + description = "When you press this key you'll hop to the next world", + position = 1 + ) + default Keybind nextKey() + { + return new Keybind(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK); + } + + @ConfigItem( + keyName = "quickhopOutOfDanger", + name = "Quick-hop out of dangerous worlds", + description = "Don't hop to a PVP/high risk world when quick-hopping", + position = 2 + ) + default boolean quickhopOutOfDanger() + { + return true; + } + + @ConfigItem( + keyName = "showSidebar", + name = "Show world hopper sidebar", + description = "Show sidebar containing all worlds that mimics in-game interface", + position = 3 + ) + default boolean showSidebar() + { + 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 new file mode 100644 index 0000000000..1db1874b26 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldHopperPlugin.java @@ -0,0 +1,543 @@ +/* + * Copyright (c) 2017, Adam + * Copyright (c) 2018, Lotto + * 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; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ObjectArrays; +import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import javax.swing.SwingUtilities; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.ChatPlayer; +import net.runelite.api.ClanMember; +import net.runelite.api.Client; +import net.runelite.api.Friend; +import net.runelite.api.GameState; +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.MenuEntryAdded; +import net.runelite.api.events.PlayerMenuOptionClicked; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.input.KeyManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.HotkeyListener; +import net.runelite.client.util.Text; +import net.runelite.client.util.WorldUtil; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldClient; +import net.runelite.http.api.worlds.WorldResult; +import net.runelite.http.api.worlds.WorldType; +import org.apache.commons.lang3.ArrayUtils; + +@PluginDescriptor( + name = "World Hopper", + description = "Allows you to quickly hop worlds" +) +@Slf4j +public class WorldHopperPlugin extends Plugin +{ + 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); + private static final ImmutableList AFTER_OPTIONS = ImmutableList.of("Message"); + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Inject + private ConfigManager configManager; + + @Inject + private ClientToolbar clientToolbar; + + @Inject + private KeyManager keyManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private ScheduledExecutorService executorService; + + @Inject + private WorldHopperConfig config; + + private NavigationButton navButton; + private WorldSwitcherPanel panel; + + private int favoriteWorld1, favoriteWorld2; + private Future worldResultFuture; + private WorldResult worldResult; + private final HotkeyListener previousKeyListener = new HotkeyListener(() -> config.previousKey()) + { + @Override + public void hotkeyPressed() + { + hop(true); + } + }; + private final HotkeyListener nextKeyListener = new HotkeyListener(() -> config.nextKey()) + { + @Override + public void hotkeyPressed() + { + hop(false); + } + }; + + @Provides + WorldHopperConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(WorldHopperConfig.class); + } + + @Override + protected void startUp() throws Exception + { + 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); + } + }); + + panel = new WorldSwitcherPanel(this); + + final BufferedImage icon; + synchronized (ImageIO.class) + { + icon = ImageIO.read(getClass().getResourceAsStream("icon.png")); + } + + navButton = NavigationButton.builder() + .tooltip("World Switcher") + .icon(icon) + .priority(3) + .panel(panel) + .build(); + + if (config.showSidebar()) + { + clientToolbar.addNavigation(navButton); + } + } + + @Override + protected void shutDown() throws Exception + { + keyManager.unregisterKeyListener(previousKeyListener); + keyManager.unregisterKeyListener(nextKeyListener); + + worldResultFuture.cancel(true); + worldResultFuture = null; + worldResult = null; + + clientToolbar.removeNavigation(navButton); + } + + @Subscribe + public void onConfigChanged(final ConfigChanged event) + { + if (event.getGroup().equals(WorldHopperConfig.GROUP) && event.getKey().equals("showSidebar")) + { + if (config.showSidebar()) + { + clientToolbar.addNavigation(navButton); + } + else + { + clientToolbar.removeNavigation(navButton); + } + } + } + + private void setFavoriteConfig(int world) + { + configManager.setConfiguration(WorldHopperConfig.GROUP, "favorite_" + world, true); + } + + private boolean isFavoriteConfig(int world) + { + Boolean favorite = configManager.getConfiguration(WorldHopperConfig.GROUP, "favorite_" + world, Boolean.class); + return favorite != null && favorite; + } + + private void clearFavoriteConfig(int world) + { + configManager.unsetConfiguration(WorldHopperConfig.GROUP, "favorite_" + world); + } + + boolean isFavorite(World world) + { + int id = world.getId(); + return id == favoriteWorld1 || id == favoriteWorld2 || isFavoriteConfig(id); + } + + int getCurrentWorld() + { + return client.getWorld(); + } + + void hopTo(World world) + { + hop(world.getId()); + } + + void addToFavorites(World world) + { + log.debug("Adding world {} to favorites", world.getId()); + setFavoriteConfig(world.getId()); + } + + void removeFromFavorites(World world) + { + log.debug("Removing world {} from favorites", world.getId()); + clearFavoriteConfig(world.getId()); + } + + @Subscribe + public void onVarbitChanged(VarbitChanged varbitChanged) + { + int old1 = favoriteWorld1; + int old2 = favoriteWorld2; + + favoriteWorld1 = client.getVar(Varbits.WORLDHOPPER_FAVROITE_1); + favoriteWorld2 = client.getVar(Varbits.WORLDHOPPER_FAVROITE_2); + + if (old1 != favoriteWorld1 || old2 != favoriteWorld2) + { + SwingUtilities.invokeLater(panel::updateList); + } + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded event) + { + int groupId = WidgetInfo.TO_GROUP(event.getActionParam1()); + String option = event.getOption(); + + if (groupId == WidgetInfo.FRIENDS_LIST.getGroupId() || groupId == WidgetInfo.CLAN_CHAT.getGroupId() || + groupId == WidgetInfo.CHATBOX.getGroupId() && !KICK_OPTION.equals(option) || //prevent from adding for Kick option (interferes with the raiding party one) + groupId == WidgetInfo.RAIDING_PARTY.getGroupId() || groupId == WidgetInfo.PRIVATE_CHAT_MESSAGE.getGroupId()) + { + boolean after; + + if (AFTER_OPTIONS.contains(option)) + { + after = true; + } + else if (BEFORE_OPTIONS.contains(option)) + { + after = false; + } + else + { + return; + } + + // Don't add entry if user is offline + ChatPlayer player = getChatPlayerFromName(event.getTarget()); + + if (player == null || player.getWorld() == 0 || player.getWorld() == client.getWorld()) + { + return; + } + + final MenuEntry hopTo = new MenuEntry(); + hopTo.setOption(HOP_TO); + hopTo.setTarget(event.getTarget()); + hopTo.setType(MenuAction.RUNELITE.getId()); + hopTo.setParam0(event.getActionParam0()); + hopTo.setParam1(event.getActionParam1()); + + insertMenuEntry(hopTo, client.getMenuEntries(), after); + } + } + + private void insertMenuEntry(MenuEntry newEntry, MenuEntry[] entries, boolean after) + { + MenuEntry[] newMenu = ObjectArrays.concat(entries, newEntry); + + if (after) + { + int menuEntryCount = newMenu.length; + ArrayUtils.swap(newMenu, menuEntryCount - 1, menuEntryCount - 2); + } + + client.setMenuEntries(newMenu); + } + + @Subscribe + public void onPlayerMenuOptionClicked(PlayerMenuOptionClicked event) + { + if (!event.getMenuOption().equals(HOP_TO)) + { + return; + } + + ChatPlayer player = getChatPlayerFromName(event.getMenuTarget()); + + if (player != null) + { + hop(player.getWorld()); + } + } + + private void hop(boolean previous) + { + if (worldResult == null || client.getGameState() != GameState.LOGGED_IN) + { + return; + } + + World currentWorld = worldResult.findWorld(client.getWorld()); + + if (currentWorld == null) + { + return; + } + + EnumSet currentWorldTypes = currentWorld.getTypes().clone(); + // Make it so you always hop out of PVP and high risk worlds + if (config.quickhopOutOfDanger()) + { + currentWorldTypes.remove(WorldType.PVP); + currentWorldTypes.remove(WorldType.PVP_HIGH_RISK); + } + // Don't regard skill total and bounty worlds as a type that must be hopped between + currentWorldTypes.remove(WorldType.BOUNTY); + currentWorldTypes.remove(WorldType.SKILL_TOTAL); + // Allow hopping from a high risk world to a non-high risk world + currentWorldTypes.remove(WorldType.PVP_HIGH_RISK); + + List worlds = worldResult.getWorlds(); + + int worldIdx = worlds.indexOf(currentWorld); + int totalLevel = client.getTotalLevel(); + + World world; + do + { + /* + Get the previous or next world in the list, + starting over at the other end of the list + if there are no more elements in the + current direction of iteration. + */ + if (previous) + { + worldIdx--; + + if (worldIdx < 0) + { + worldIdx = worlds.size() - 1; + } + } + else + { + worldIdx++; + + if (worldIdx >= worlds.size()) + { + worldIdx = 0; + } + } + + world = worlds.get(worldIdx); + + EnumSet types = world.getTypes().clone(); + + types.remove(WorldType.BOUNTY); + + if (types.contains(WorldType.SKILL_TOTAL)) + { + try + { + int totalRequirement = Integer.parseInt(world.getActivity().substring(0, world.getActivity().indexOf(" "))); + + if (totalLevel >= totalRequirement) + { + types.remove(WorldType.SKILL_TOTAL); + } + } + catch (NumberFormatException ex) + { + log.warn("Failed to parse total level requirement for target world", ex); + } + } + + // Break out if we've found a good world to hop to + if (currentWorldTypes.equals(types)) + { + break; + } + } + while (world != currentWorld); + + if (world == currentWorld) + { + String chatMessage = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Couldn't find a world to quick-hop to.") + .build(); + + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAME) + .runeLiteFormattedMessage(chatMessage) + .build()); + } + else + { + hop(world.getId()); + } + } + + private void hop(int worldId) + { + // Don't try to hop if the world doesn't exist + World world = worldResult.findWorld(worldId); + if (world == null) + { + return; + } + + final net.runelite.api.World rsWorld = client.createWorld(); + rsWorld.setActivity(world.getActivity()); + rsWorld.setAddress(world.getAddress()); + rsWorld.setId(world.getId()); + rsWorld.setPlayerCount(world.getPlayers()); + rsWorld.setLocation(world.getLocation()); + rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); + + if (client.getGameState() == GameState.LOGIN_SCREEN) + { + // on the login screen we can just change the world by ourselves + client.changeWorld(rsWorld); + return; + } + + String chatMessage = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Quick-hopping to World ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(world.getId())) + .append(ChatColorType.NORMAL) + .append("..") + .build(); + + chatMessageManager + .queue(QueuedMessage.builder() + .type(ChatMessageType.GAME) + .runeLiteFormattedMessage(chatMessage) + .build()); + + clientThread.invokeLater(() -> + { + if (client.getWidget(WidgetInfo.WORLD_SWITCHER_LIST) == null) + { + client.openWorldHopper(); + return false; + } + + client.hopToWorld(rsWorld); + return true; + }); + } + + private ChatPlayer getChatPlayerFromName(String name) + { + String cleanName = Text.removeTags(name); + + Friend[] friends = client.getFriends(); + + if (friends != null) + { + for (Friend friend : friends) + { + if (friend != null && friend.getName().equals(cleanName)) + { + return friend; + } + } + } + + ClanMember[] clanMembers = client.getClanMembers(); + + if (clanMembers != null) + { + for (ClanMember clanMember : clanMembers) + { + if (clanMember != null && clanMember.getUsername().equals(cleanName)) + { + return clanMember; + } + } + } + + return null; + } +} 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 new file mode 100644 index 0000000000..befd77ffb1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldSwitcherPanel.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2018, Psikoi + * 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; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.PluginPanel; +import net.runelite.http.api.worlds.World; + +@Slf4j +class WorldSwitcherPanel extends PluginPanel +{ + private static final Color ODD_ROW = new Color(44, 44, 44); + + private static final int WORLD_COLUMN_WIDTH = 60; + private static final int PLAYERS_COLUMN_WIDTH = 40; + + private final JPanel listContainer = new JPanel(); + + private WorldTableHeader worldHeader; + private WorldTableHeader playersHeader; + private WorldTableHeader activityHeader; + + private WorldOrder orderIndex = WorldOrder.WORLD; + private boolean ascendingOrder = true; + + private List worlds; + private WorldHopperPlugin plugin; + + WorldSwitcherPanel(WorldHopperPlugin plugin) + { + this.plugin = plugin; + + setBorder(null); + setLayout(new DynamicGridLayout(0, 1)); + + JPanel headerContainer = buildHeader(); + + listContainer.setLayout(new GridLayout(0, 1)); + + add(headerContainer); + add(listContainer); + } + + void updateList() + { + worlds.sort((w1, w2) -> + { + switch (orderIndex) + { + case WORLD: + return Integer.compare(w1.getId(), w2.getId()) * (ascendingOrder ? 1 : -1); + case PLAYERS: + return Integer.compare(w1.getPlayers(), w2.getPlayers()) * (ascendingOrder ? 1 : -1); + case ACTIVITY: + return w1.getActivity().compareTo(w2.getActivity()) * (ascendingOrder ? 1 : -1); + default: + return 0; + + } + }); + + worlds.sort((w1, w2) -> + { + boolean b1 = plugin.isFavorite(w1); + boolean b2 = plugin.isFavorite(w2); + return Boolean.compare(b2, b1); + }); + + listContainer.removeAll(); + + for (int i = 0; i < worlds.size(); i++) + { + World world = worlds.get(i); + listContainer.add(buildRow(world, i % 2 == 0, world.getId() == plugin.getCurrentWorld(), plugin.isFavorite(world))); + } + + listContainer.revalidate(); + listContainer.repaint(); + } + + void populate(List worlds) + { + this.worlds = new ArrayList<>(worlds); + updateList(); + } + + private void orderBy(WorldOrder order) + { + worldHeader.highlight(false, ascendingOrder); + playersHeader.highlight(false, ascendingOrder); + activityHeader.highlight(false, ascendingOrder); + + switch (order) + { + case WORLD: + worldHeader.highlight(true, ascendingOrder); + break; + case PLAYERS: + playersHeader.highlight(true, ascendingOrder); + break; + case ACTIVITY: + activityHeader.highlight(true, ascendingOrder); + break; + } + + orderIndex = order; + updateList(); + } + + /** + * Builds the entire table header. + */ + private JPanel buildHeader() + { + JPanel header = new JPanel(new BorderLayout()); + JPanel leftSide = new JPanel(new BorderLayout()); + + worldHeader = new WorldTableHeader("World", orderIndex == WorldOrder.WORLD, ascendingOrder); + worldHeader.setPreferredSize(new Dimension(WORLD_COLUMN_WIDTH, 0)); + worldHeader.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + ascendingOrder = orderIndex != WorldOrder.WORLD || !ascendingOrder; + orderBy(WorldOrder.WORLD); + } + }); + + playersHeader = new WorldTableHeader("#", orderIndex == WorldOrder.PLAYERS, ascendingOrder); + playersHeader.setPreferredSize(new Dimension(PLAYERS_COLUMN_WIDTH, 0)); + playersHeader.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + + ascendingOrder = orderIndex != WorldOrder.PLAYERS || !ascendingOrder; + orderBy(WorldOrder.PLAYERS); + } + }); + + activityHeader = new WorldTableHeader("Activity", orderIndex == WorldOrder.ACTIVITY, ascendingOrder); + activityHeader.setBorder(new EmptyBorder(3, 5, 3, 5)); + activityHeader.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + ascendingOrder = orderIndex != WorldOrder.ACTIVITY || !ascendingOrder; + orderBy(WorldOrder.ACTIVITY); + } + }); + + leftSide.add(worldHeader, BorderLayout.WEST); + leftSide.add(playersHeader, BorderLayout.EAST); + + header.add(leftSide, BorderLayout.WEST); + header.add(activityHeader, BorderLayout.CENTER); + + return header; + } + + /** + * Builds a table row, that displays the world's information. + */ + private JPanel buildRow(World world, boolean stripe, boolean current, boolean favorite) + { + JPanel row = new WorldTableRow(world, current, favorite, + world1 -> + { + plugin.hopTo(world1); + }, + (world12, add) -> + { + if (add) + { + plugin.addToFavorites(world12); + } + else + { + plugin.removeFromFavorites(world12); + } + + updateList(); + } + ); + row.setBackground(stripe ? ODD_ROW : ColorScheme.DARK_GRAY_COLOR); + return row; + } + + /** + * Enumerates the multiple ordering options for the world list. + */ + private enum WorldOrder + { + WORLD, + PLAYERS, + ACTIVITY, + PING + } +} 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 new file mode 100644 index 0000000000..d820fcd44a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2018, Psikoi + * 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; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.image.BufferedImage; +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.util.ImageUtil; + +class WorldTableHeader extends JPanel +{ + private static final ImageIcon ARROW_DOWN; + private static final ImageIcon ARROW_UP; + private static final ImageIcon ARROW_UP_FADED; + + static + { + final BufferedImage arrowDown = ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "arrow_down.png"); + final BufferedImage arrowUp = ImageUtil.rotateImage(arrowDown, Math.PI); + final BufferedImage arrowUpFaded = ImageUtil.grayscaleOffset(arrowUp, -80); + ARROW_DOWN = new ImageIcon(arrowDown); + ARROW_UP = new ImageIcon(arrowUp); + ARROW_UP_FADED = new ImageIcon(arrowUpFaded); + } + + private final JLabel textLabel = new JLabel(); + private final JLabel arrowLabel = new JLabel(); + // Determines if this header column is being used to order the list + private boolean ordering = false; + + WorldTableHeader(String title, boolean ordered, boolean ascending) + { + setLayout(new BorderLayout(5, 0)); + setBorder(new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 0, 1, ColorScheme.MEDIUM_GRAY_COLOR), + new EmptyBorder(0, 5, 0, 5))); + setBackground(ColorScheme.SCROLL_TRACK_COLOR); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + textLabel.setForeground(Color.WHITE); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + if (ordering) + { + textLabel.setForeground(Color.WHITE); + return; + } + + textLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + } + }); + + textLabel.setText(title); + textLabel.setFont(FontManager.getRunescapeSmallFont()); + + highlight(ordered, ascending); + + add(textLabel, BorderLayout.WEST); + add(arrowLabel, BorderLayout.EAST); + } + + /** + * The labels inherit the parent's mouse listeners. + */ + @Override + public void addMouseListener(MouseListener mouseListener) + { + super.addMouseListener(mouseListener); + textLabel.addMouseListener(mouseListener); + arrowLabel.addMouseListener(mouseListener); + } + + /** + * If this column header is being used to order, then it should be + * highlighted, changing it's font color and icon. + */ + public void highlight(boolean on, boolean ascending) + { + ordering = on; + arrowLabel.setIcon(on ? (ascending ? ARROW_DOWN : ARROW_UP) : ARROW_UP_FADED); + textLabel.setForeground(on ? Color.WHITE : ColorScheme.LIGHT_GRAY_COLOR); + } +} 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 new file mode 100644 index 0000000000..a61aa21aaf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableRow.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2018, Psikoi + * 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; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.FontManager; +import net.runelite.client.util.ImageUtil; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldType; + +class WorldTableRow extends JPanel +{ + private static final ImageIcon FLAG_AUS; + private static final ImageIcon FLAG_UK; + private static final ImageIcon FLAG_US; + private static final ImageIcon FLAG_GER; + + private static final int WORLD_COLUMN_WIDTH = 60; + private static final int PLAYERS_COLUMN_WIDTH = 40; + + private static final Color CURRENT_WORLD = new Color(66, 227, 17); + private static final Color UNAVAILABLE_WORLD = Color.GRAY.darker().darker(); + private static final Color DANGEROUS_WORLD = new Color(251, 62, 62); + private static final Color TOURNAMENT_WORLD = new Color(79, 145, 255); + private static final Color MEMBERS_WORLD = new Color(210, 193, 53); + private static final Color FREE_WORLD = new Color(200, 200, 200); + + static + { + FLAG_AUS = new ImageIcon(ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "flag_aus.png")); + FLAG_UK = new ImageIcon(ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "flag_uk.png")); + FLAG_US = new ImageIcon(ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "flag_us.png")); + FLAG_GER = new ImageIcon(ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "flag_ger.png")); + } + + private Color lastBackground; + private boolean current; + + WorldTableRow(World world, boolean current, boolean favorite, Consumer onSelect, BiConsumer onFavorite) + { + this.current = current; + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(2, 0, 2, 0)); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent mouseEvent) + { + if (mouseEvent.getClickCount() == 2) + { + if (onSelect != null) + { + onSelect.accept(world); + } + } + } + + @Override + public void mousePressed(MouseEvent mouseEvent) + { + if (mouseEvent.getClickCount() == 2) + { + setBackground(getBackground().brighter()); + } + } + + @Override + public void mouseReleased(MouseEvent mouseEvent) + { + if (mouseEvent.getClickCount() == 2) + { + setBackground(getBackground().darker()); + } + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + WorldTableRow.this.lastBackground = getBackground(); + setBackground(getBackground().brighter()); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + setBackground(lastBackground); + } + }); + + String favoriteAction = favorite ? + "Remove " + world.getId() + " from favorites" : + "Add " + world.getId() + " to favorites"; + + final JMenuItem fav = new JMenuItem(favoriteAction); + fav.addActionListener(e -> + { + onFavorite.accept(world, !favorite); + }); + + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(fav); + + setComponentPopupMenu(popupMenu); + + JPanel leftSide = new JPanel(new BorderLayout()); + leftSide.setOpaque(false); + + JPanel worldField = buildWorldField(world); + worldField.setPreferredSize(new Dimension(WORLD_COLUMN_WIDTH, 0)); + worldField.setOpaque(false); + + JPanel playersField = buildPlayersField(world); + playersField.setPreferredSize(new Dimension(PLAYERS_COLUMN_WIDTH, 0)); + playersField.setOpaque(false); + + JPanel activityField = buildActivityField(world); + activityField.setBorder(new EmptyBorder(5, 5, 5, 5)); + activityField.setOpaque(false); + + leftSide.add(worldField, BorderLayout.WEST); + leftSide.add(playersField, BorderLayout.EAST); + + add(leftSide, BorderLayout.WEST); + add(activityField, BorderLayout.CENTER); + } + + /** + * Builds the players list field (containing the amount of players logged in that world). + */ + private JPanel buildPlayersField(World world) + { + 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); + + column.add(label, BorderLayout.WEST); + + return column; + } + + /** + * Builds the activity list field (containing that world's activity/theme). + */ + private JPanel buildActivityField(World world) + { + JPanel column = new JPanel(new BorderLayout()); + column.setBorder(new EmptyBorder(0, 5, 0, 5)); + + JLabel label = new JLabel(world.getActivity()); + label.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); + + return column; + } + + /** + * Builds the world list field (containing the country's flag and the world index). + */ + private JPanel buildWorldField(World world) + { + 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); + } + + JLabel flag = new JLabel(getFlag(world.getLocation())); + + column.add(flag, BorderLayout.WEST); + column.add(label, BorderLayout.CENTER); + + return column; + } + + private ImageIcon getFlag(int locationId) + { + switch (locationId) + { + case 0: + return FLAG_US; + case 1: + return FLAG_UK; + case 3: + return FLAG_AUS; + default: + return FLAG_GER; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/util/WorldUtil.java b/runelite-client/src/main/java/net/runelite/client/util/WorldUtil.java new file mode 100644 index 0000000000..9f508676a5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/WorldUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.util; + +import java.util.EnumSet; +import net.runelite.api.WorldType; + +/** + * Utility class for RuneScape worlds + */ +public class WorldUtil +{ + /** + * Converts http-api world types to runelite-api world types + * TODO: Find a better way to handle these to not have duplicate interfaces + * @param apiTypes http-api world types + * @return runelite-api world types + */ + public static EnumSet toWorldTypes(final EnumSet apiTypes) + { + final EnumSet types = EnumSet.noneOf(net.runelite.api.WorldType.class); + + for (net.runelite.http.api.worlds.WorldType apiType : apiTypes) + { + types.add(net.runelite.api.WorldType.valueOf(apiType.name())); + } + + return types; + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/arrow_down.png b/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1fc3d7cae323f13ef542c927b89eb2c9a4e2a2 GIT binary patch literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|m6g*uVLp07OCrGfWH2vrOKhcpn lF_pK)H(Mbm^Y96-G=^k1<%fbPJYGPx44$rjF6*2UngD;m7W)7I literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_aus.png b/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_aus.png new file mode 100644 index 0000000000000000000000000000000000000000..202f3927d9d86c93306290648037cb2b27a40168 GIT binary patch literal 534 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfLzW3kH}&m zoe#o{9~a$m1~Mc|Tq8#cPKl&lQpvj&()Xxl?N!U(tDV1Jw_v|v(E-=` zqaKaN{M$|hwx5XYKb<(~Oxo15xwFm}%(+l9?_$l8%S|h;bg#R<ei4y%zw6c V{*8`aV+V9OgQu&X%Q~loCIGtS@N578 literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_ger.png b/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_ger.png new file mode 100644 index 0000000000000000000000000000000000000000..cb212ef82869cca75a75b93ac9303027049ca6c6 GIT binary patch literal 433 zcmV;i0Z#sjP)D871~>9C(O9lVpp^Nty#Ir$z&$?d~-7yRFyJ71SG&5cr2I~XVJI* zlGp&Qfi0lFuq}ETp8@~}Oe(~$OaNC77}b#985J%uu=z*828iXBqv1aG-h&HhiF^+r z7%$-SHOGP9kPYPk=_}@#>T3RUXs~??bpMd6s%v+U0kFVw2Y)H{P!_ysgy5`LX9Q=& zI0R<|2cQ8@z-v&i0iT7;6Yw<6!szNe#%3#EujcJrGJ9g_%+?qk*B;*Jn9gjSKjQP# zB`~d_PYZ`UpqF~utpR8P_zKa3CbU}vda2I`DgN_Et{b!>4PCsd!a+6JmJ93*?!cM8=-G)U6&fR)(xw!WRgZj;i z{+kb9J*nt<);st8qbKiQyyC7nRs}SWt0c%Tn1PYibluh+J1_qGYxn&Xl!-tqw+Hr2#E-*BqtbmY|kzw&E1d5m}L%B=-Dh{4m<&t;ucLK6Uv Cahikx literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_us.png b/runelite-client/src/main/resources/net/runelite/client/plugins/worldhopper/flag_us.png new file mode 100644 index 0000000000000000000000000000000000000000..96929a20e61fdc50e869853a083efa445c837feb GIT binary patch literal 589 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfLzW3kH}&m zoe#o{9~a$m1~Mc|Tq8a17{DT*t+;>lMtU2u0cKp6;B2db*;?P}vzuPLVKsB#6?|tAL z&lj`(7N7AgVKa{KZNhOo8G^Pj2XA2x*~${Kl{IuLd*~MS@NGaTuE_2DF*|??WK(x5 zW$e+)+pk-&->CS2Ny$O0io>>5hh6KBx;GpP?mQXMb1Ht~nZh|2I@et5U4MPazPoFW z+~0ln@eKy`8_b%w#4PXV_}sIOe6?lY>%*ts-MkBQi4gmLQJ}{)N`m}?85o(EnAz0q z92}f#8`?T1Pg}Bn^VZ#a_8mR>@X531uRnbI@#pW~zyJOY3;1;usp-DNbKM1Re%~wb_y2Q`{9`XkJ?RDvg3R=Cm zjb;jdUM<5iVgCH%t1mxzc#`Sj!!I7w*dJA7=@c#g=w`m{)@-lE32X9y{kvqmQp>8= zS9O+`&nniNA15BPwzE01yT4;b>c5Ri4zIqdvo;9Mk#B75ekkSd-<<5=sRID$dtM;P*y%m*63g!Xy9c#pWlAgyWVf@J$`mKlU(<6aU=&z zVn(Q5k>CG(l5--(dAeH4dq$;ZU{X51{8nWbJ0$n)9?5V;3wtOKeQ?LtilpX+OKMS` z+!Q>q2%0QkDjsa{l_BDJ0yGPWpE*KuAQ8Wf$W&s1<-bB4IvvlWg?F2sv7 zD{L*C#r*O_%?)T-L$ciNbIawHd@t12aZ4P4iGw1nTP!|sfc&HNiAQtAIZkjm^6QVT zRkl+Q017RDbh?=1#|rgA;n_ z32Bkyb&CKS{N?vK=^lElP_m|V%aa4cvE_`3f|^PlOB~}~T?54t8*m8x+4QW62*{Ro zV5?mlnAoBc{S{n#Z(7140+wAEo=N>wd5_T0weX0D1J09iEOHv&X3(t#C{Ez?9UE&gso2U>pwPWNSF&3dru~ zibey)GEnBNPpZRp<0_)tolvjP z@T39R71g})JDGNXn?m=cNqknZ#H6Gv3^|UEilgwo3TR-2XKTMA" + (world - 300) + "", 683, 244); + final int worldId = world.getId(); + menuAction(worldId, WidgetInfo.WORLD_SWITCHER_LIST.getId(), MenuAction.WIDGET_DEFAULT.getId(), 1, "Switch", "" + (worldId - 300) + "", 683, 244); } }