runelite-client: add world hopper plugin

Co-authored-by: Adam <Adam@anope.org>
Co-authored-by: psikoi <ruben.amendoeira@gmail.com>
Co-authored-by: Tomas Slusny <slusnucky@gmail.com>
This commit is contained in:
Lotto
2018-08-07 19:53:29 +01:00
committed by Adam
parent 40ff55558f
commit 12da232985
17 changed files with 1309 additions and 20 deletions

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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<WorldType> toWorldTypes(final EnumSet<net.runelite.http.api.worlds.WorldType> apiTypes)
{
final EnumSet<WorldType> 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)

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}
}

View File

@@ -0,0 +1,543 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<String> BEFORE_OPTIONS = ImmutableList.of("Add friend", "Remove friend", KICK_OPTION);
private static final ImmutableList<String> 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<WorldType> 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<World> 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<WorldType> 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;
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright (c) 2018, Psikoi <https://github.com/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<World> 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<World> 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
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (c) 2018, Psikoi <https://github.com/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);
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright (c) 2018, Psikoi <https://github.com/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<World> onSelect, BiConsumer<World, Boolean> 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;
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* 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<WorldType> toWorldTypes(final EnumSet<net.runelite.http.api.worlds.WorldType> apiTypes)
{
final EnumSet<net.runelite.api.WorldType> 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

View File

@@ -25,6 +25,7 @@
package net.runelite.mixins;
import net.runelite.api.MenuAction;
import net.runelite.api.World;
import net.runelite.api.mixins.Inject;
import net.runelite.api.mixins.Mixin;
import net.runelite.api.widgets.WidgetInfo;
@@ -43,8 +44,9 @@ public abstract class WorldHoppingMixin implements RSClient
@Inject
@Override
public void hopToWorld(int world)
public void hopToWorld(World world)
{
menuAction(world, WidgetInfo.WORLD_SWITCHER_LIST.getId(), MenuAction.WIDGET_DEFAULT.getId(), 1, "Switch", "<col=ff9040>" + (world - 300) + "</col>", 683, 244);
final int worldId = world.getId();
menuAction(worldId, WidgetInfo.WORLD_SWITCHER_LIST.getId(), MenuAction.WIDGET_DEFAULT.getId(), 1, "Switch", "<col=ff9040>" + (worldId - 300) + "</col>", 683, 244);
}
}