diff --git a/runelite-client/src/main/java/net/runelite/client/events/PartyMemberAvatar.java b/runelite-client/src/main/java/net/runelite/client/events/PartyMemberAvatar.java new file mode 100644 index 0000000000..d1c44c87d5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/PartyMemberAvatar.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021, Jonathan Rousseau + * 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.events; + +import java.awt.image.BufferedImage; +import java.util.UUID; +import lombok.Value; + +@Value +public class PartyMemberAvatar +{ + private final UUID memberId; + private final BufferedImage image; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java index 966cc381c7..a4bb4c7d1e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java @@ -1,6 +1,7 @@ /* * Copyright (c) 2018, Tomas Slusny * Copyright (c) 2018, PandahRS + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -50,7 +51,6 @@ import net.runelite.api.events.StatChanged; import net.runelite.client.config.ConfigManager; import net.runelite.client.discord.DiscordService; import net.runelite.client.discord.events.DiscordJoinGame; -import net.runelite.client.discord.events.DiscordJoinRequest; import net.runelite.client.discord.events.DiscordReady; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; @@ -215,20 +215,6 @@ public class DiscordPlugin extends Plugin partyService.setUsername(event.getUsername() + "#" + event.getDiscriminator()); } - @Subscribe - public void onDiscordJoinRequest(DiscordJoinRequest request) - { - // In order for the "Invite to join" message to work we need to have a valid party in Discord presence. - // We lazily create the party here in order to avoid the (1 of 15) being permanently in the Discord status. - if (!partyService.isInParty()) - { - // Change to my party id, which is advertised in the Discord presence secret. This will open the socket, - // send a join, and cause a UserJoin later for me, which will then update the presence and allow the - // "Invite to join" to continue. - partyService.changeParty(partyService.getLocalPartyId()); - } - } - @Subscribe public void onDiscordJoinGame(DiscordJoinGame joinGame) { @@ -303,7 +289,8 @@ public class DiscordPlugin extends Plugin { image = ImageIO.read(inputStream); } - memberById.setAvatar(image); + + partyService.setPartyMemberAvatar(memberById.getMemberId(), image); } finally { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java index f311511d26..b9039cee6d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,7 +32,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import javax.inject.Inject; @@ -55,7 +55,6 @@ class DiscordState private Instant updated; } - private final UUID partyId = UUID.randomUUID(); private final List events = new ArrayList<>(); private final DiscordService discordService; private final DiscordConfig config; @@ -106,15 +105,10 @@ class DiscordState .largeImageText(lastPresence.getLargeImageText()) .startTimestamp(lastPresence.getStartTimestamp()) .smallImageKey(lastPresence.getSmallImageKey()) - .partyMax(lastPresence.getPartyMax()) - .partySize(party.getMembers().size()); + .partyMax(lastPresence.getPartyMax()); - if (!party.isInParty() || party.isPartyOwner()) - { - // This is only used to identify the invites on Discord's side. Our party ids are the secret. - presenceBuilder.partyId(partyId.toString()); - presenceBuilder.joinSecret(party.getLocalPartyId().toString()); - } + + setPresencePartyInfo(presenceBuilder); discordService.updatePresence(presenceBuilder.build()); } @@ -205,8 +199,7 @@ class DiscordState .details(MoreObjects.firstNonNull(details, "")) .largeImageText(runeliteTitle + " v" + versionShortHand) .smallImageKey(imageKey) - .partyMax(PARTY_MAX) - .partySize(party.getMembers().size()); + .partyMax(PARTY_MAX); final Instant startTime; switch (config.elapsedTimeType()) @@ -233,11 +226,7 @@ class DiscordState presenceBuilder.startTimestamp(startTime); - if (!party.isInParty() || party.isPartyOwner()) - { - presenceBuilder.partyId(partyId.toString()); - presenceBuilder.joinSecret(party.getLocalPartyId().toString()); - } + setPresencePartyInfo(presenceBuilder); final DiscordPresence presence = presenceBuilder.build(); @@ -286,4 +275,16 @@ class DiscordState updatePresenceWithLatestEvent(); } } + + private void setPresencePartyInfo(DiscordPresence.DiscordPresenceBuilder presenceBuilder) + { + if (party.isInParty()) + { + presenceBuilder.partySize(party.getMembers().size()); + + // Set public party id and secret + presenceBuilder.partyId(party.getPublicPartyId().toString()); + presenceBuilder.joinSecret(party.getPartyId().toString()); + } + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyConfig.java index cf33302d21..5d38da1b3d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyConfig.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,23 +29,16 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; -@ConfigGroup("party") +@ConfigGroup(PartyConfig.GROUP) public interface PartyConfig extends Config { - @ConfigItem( - keyName = "stats", - name = "Stats", - description = "Enables party stats overlay showing HP, prayer and player name" - ) - default boolean stats() - { - return true; - } + String GROUP = "party"; @ConfigItem( keyName = "pings", name = "Pings", - description = "Enables party pings (shift + left-click)" + description = "Enables party pings (shift + left-click)", + position = 1 ) default boolean pings() { @@ -54,7 +48,8 @@ public interface PartyConfig extends Config @ConfigItem( keyName = "sounds", name = "Sound on ping", - description = "Enables sound notification on party ping" + description = "Enables sound notification on party ping", + position = 2 ) default boolean sounds() { @@ -64,7 +59,8 @@ public interface PartyConfig extends Config @ConfigItem( keyName = "messages", name = "Join messages", - description = "Enables join/leave game messages" + description = "Enables members join/leave game messages", + position = 3 ) default boolean messages() { @@ -74,10 +70,33 @@ public interface PartyConfig extends Config @ConfigItem( keyName = "recolorNames", name = "Recolor names", - description = "Recolor stats overlay names based on unique color hash" + description = "Recolor party members names based on unique color hash", + position = 4 ) default boolean recolorNames() { return true; } + + @ConfigItem( + keyName = "autoOverlay", + name = "Auto overlay", + description = "Automatically add an overlay with player data when a member joins", + position = 5 + ) + default boolean autoOverlay() + { + return true; + } + + @ConfigItem( + keyName = "includeSelf", + name = "Include yourself", + description = "Shows yourself in the panel as part of the party", + position = 6 + ) + default boolean includeSelf() + { + return false; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyMemberBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyMemberBox.java new file mode 100644 index 0000000000..7b3839b406 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyMemberBox.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2021, Jonathan Rousseau + * 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.party; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingConstants; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.client.plugins.party.data.PartyData; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.MouseDragEventForwarder; +import net.runelite.client.ui.components.ProgressBar; +import net.runelite.client.util.ImageUtil; + +class PartyMemberBox extends JPanel +{ + private static final Color HP_FG = new Color(0, 146, 54, 230); + private static final Color HP_BG = new Color(102, 15, 16, 230); + private static final Color PRAY_FG = new Color(0, 149, 151); + private static final Color PRAY_BG = Color.black; + + @Getter(AccessLevel.PACKAGE) + private final PartyData memberPartyData; + + private final ProgressBar hpBar = new ProgressBar(); + private final ProgressBar prayerBar = new ProgressBar(); + + private final JLabel topName = new JLabel(); + private final JLabel bottomName = new JLabel(); + + private final JLabel avatar = new JLabel(); + + private final PartyConfig config; + + private boolean avatarSet; + + PartyMemberBox(final PartyConfig config, final JComponent panel, final PartyData memberPartyData) + { + this.config = config; + this.memberPartyData = memberPartyData; + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(5, 0, 0, 0)); + + /* The box's wrapping container */ + final JPanel container = new JPanel(); + container.setLayout(new BorderLayout()); + container.setBackground(ColorScheme.DARKER_GRAY_COLOR); + container.setBorder(new EmptyBorder(5, 5, 5, 5)); + + // Create Toggle overlay + final JMenuItem overlay = new JMenuItem("Toggle overlay"); + overlay.addActionListener(e -> memberPartyData.setShowOverlay(!memberPartyData.isShowOverlay())); + + // Create popup menu + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(overlay); + + // create a line border with the specified color and width + Border border = BorderFactory.createLineBorder(Color.gray, 1); + avatar.setBorder(border); + + avatar.setHorizontalAlignment(SwingConstants.CENTER); + avatar.setVerticalAlignment(SwingConstants.CENTER); + avatar.setPreferredSize(new Dimension(35, 35)); + + /* Contains the avatar and the names */ + final JPanel headerPanel = new JPanel(); + headerPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + headerPanel.setLayout(new BorderLayout()); + headerPanel.setBorder(new EmptyBorder(0, 0, 3, 0)); + + /* Contains ServiceName name and osrs name */ + final JPanel namesPanel = new JPanel(); + namesPanel.setLayout(new DynamicGridLayout(2, 1)); + namesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + namesPanel.setBorder(new EmptyBorder(2, 5, 2, 5)); + + topName.setFont(FontManager.getRunescapeSmallFont()); + bottomName.setFont(FontManager.getRunescapeSmallFont()); + + topName.putClientProperty("html.disable", Boolean.TRUE); + bottomName.putClientProperty("html.disable", Boolean.TRUE); + + namesPanel.add(topName); // top + namesPanel.add(bottomName); // bottom + + headerPanel.add(avatar, BorderLayout.WEST); + headerPanel.add(namesPanel, BorderLayout.CENTER); + + JPanel progressWrapper = new JPanel(); + progressWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR); + progressWrapper.setLayout(new DynamicGridLayout(2, 1, 0, 2)); + + hpBar.setBackground(HP_BG); + hpBar.setForeground(HP_FG); + + prayerBar.setBackground(PRAY_BG); + prayerBar.setForeground(PRAY_FG); + + progressWrapper.add(hpBar); // top + progressWrapper.add(prayerBar); // bottom + + container.add(headerPanel, BorderLayout.NORTH); + container.add(progressWrapper, BorderLayout.SOUTH); + + container.setComponentPopupMenu(popupMenu); + + // forward mouse drag events to parent panel for drag and drop reordering + MouseDragEventForwarder mouseDragEventForwarder = new MouseDragEventForwarder(panel); + container.addMouseListener(mouseDragEventForwarder); + container.addMouseMotionListener(mouseDragEventForwarder); + + add(container, BorderLayout.NORTH); + + update(); + } + + void update() + { + // Avatar + if (!avatarSet && memberPartyData.getMember().getAvatar() != null) + { + ImageIcon icon = new ImageIcon(ImageUtil.resizeImage(memberPartyData.getMember().getAvatar(), 32, 32)); + icon.getImage().flush(); + avatar.setIcon(icon); + + avatarSet = true; + } + + // Update progress bars + hpBar.setValue(memberPartyData.getHitpoints()); + hpBar.setMaximumValue(memberPartyData.getMaxHitpoints()); + hpBar.setCenterLabel(progressBarLabel(memberPartyData.getHitpoints(), memberPartyData.getMaxHitpoints())); + + prayerBar.setValue(memberPartyData.getPrayer()); + prayerBar.setMaximumValue(memberPartyData.getMaxPrayer()); + prayerBar.setCenterLabel(progressBarLabel(memberPartyData.getPrayer(), memberPartyData.getMaxPrayer())); + + // Update name labels + Color playerColor = config.recolorNames() ? memberPartyData.getColor() : Color.WHITE; + boolean isLoggedIn = !memberPartyData.getCharacterName().isEmpty(); + + topName.setForeground(playerColor); + topName.setText(memberPartyData.getMember().getName()); + + bottomName.setForeground(isLoggedIn ? playerColor : Color.GRAY); + bottomName.setText(isLoggedIn ? memberPartyData.getCharacterName() : "Logged out"); + } + + private static String progressBarLabel(int current, int max) + { + return current + "/" + max; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPanel.java new file mode 100644 index 0000000000..98cecb81bf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPanel.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2021, Jonathan Rousseau + * 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.party; + +import com.google.inject.Inject; +import java.awt.BorderLayout; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import net.runelite.client.plugins.party.data.PartyData; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.DragAndDropReorderPane; +import net.runelite.client.ui.components.PluginErrorPanel; +import net.runelite.client.ws.PartyService; + +class PartyPanel extends PluginPanel +{ + private static final String BTN_CREATE_TEXT = "Create party"; + private static final String BTN_LEAVE_TEXT = "Leave party"; + + private final PartyPlugin plugin; + private final PartyService party; + private final PartyConfig config; + + private final Map requestBoxes = new HashMap<>(); + private final Map memberBoxes = new HashMap<>(); + + private final JButton startButton = new JButton(); + + private final PluginErrorPanel noPartyPanel = new PluginErrorPanel(); + private final PluginErrorPanel partyEmptyPanel = new PluginErrorPanel(); + private final JComponent memberBoxPanel = new DragAndDropReorderPane(); + private final JComponent requestBoxPanel = new DragAndDropReorderPane(); + + @Inject + PartyPanel(final PartyPlugin plugin, final PartyConfig config, final PartyService party) + { + this.plugin = plugin; + this.party = party; + this.config = config; + + setBorder(new EmptyBorder(10, 10, 10, 10)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + setLayout(new BorderLayout()); + + final JPanel layoutPanel = new JPanel(); + BoxLayout boxLayout = new BoxLayout(layoutPanel, BoxLayout.Y_AXIS); + layoutPanel.setLayout(boxLayout); + add(layoutPanel, BorderLayout.NORTH); + + final JPanel topPanel = new JPanel(); + + topPanel.setBorder(new EmptyBorder(0, 0, 10, 0)); + topPanel.setLayout(new BorderLayout()); + + topPanel.add(startButton, BorderLayout.CENTER); + + layoutPanel.add(topPanel); + layoutPanel.add(requestBoxPanel); + layoutPanel.add(memberBoxPanel); + + startButton.setText(party.isInParty() ? BTN_LEAVE_TEXT : BTN_CREATE_TEXT); + startButton.setFocusable(false); + + topPanel.add(startButton); + + startButton.addActionListener(e -> + { + if (party.isInParty()) + { + // Leave party + final int result = JOptionPane.showOptionDialog(startButton, + "Are you sure you want to leave the party?", + "Leave party?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, + null, new String[]{"Yes", "No"}, "No"); + + if (result == JOptionPane.YES_OPTION) + { + party.changeParty(null); + } + } + else + { + // Create party + party.changeParty(party.getLocalPartyId()); + } + }); + + noPartyPanel.setContent("Not in a party", "Create a party to begin."); + partyEmptyPanel.setContent("Party created", "You can now invite friends!"); + + updateParty(); + } + + void updateParty() + { + remove(noPartyPanel); + remove(partyEmptyPanel); + + startButton.setText(party.isInParty() ? BTN_LEAVE_TEXT : BTN_CREATE_TEXT); + + if (!party.isInParty()) + { + add(noPartyPanel); + } + else if (plugin.getPartyDataMap().size() <= 1) + { + add(partyEmptyPanel); + } + } + + void addMember(PartyData partyData) + { + if (!memberBoxes.containsKey(partyData.getMember().getMemberId())) + { + PartyMemberBox partyMemberBox = new PartyMemberBox(config, memberBoxPanel, partyData); + memberBoxes.put(partyData.getMember().getMemberId(), partyMemberBox); + memberBoxPanel.add(partyMemberBox); + memberBoxPanel.revalidate(); + } + updateParty(); + } + + void removeAllMembers() + { + memberBoxes.forEach((key, value) -> memberBoxPanel.remove(value)); + memberBoxPanel.revalidate(); + memberBoxes.clear(); + updateParty(); + } + + void removeMember(UUID memberId) + { + final PartyMemberBox memberBox = memberBoxes.remove(memberId); + + if (memberBox != null) + { + memberBoxPanel.remove(memberBox); + memberBoxPanel.revalidate(); + } + + updateParty(); + } + + void updateMember(UUID userId) + { + final PartyMemberBox memberBox = memberBoxes.get(userId); + + if (memberBox != null) + { + memberBox.update(); + } + } + + void updateAll() + { + memberBoxes.forEach((key, value) -> value.update()); + } + + void addRequest(String userId, String userName) + { + PartyRequestBox partyRequestBox = new PartyRequestBox(plugin, requestBoxPanel, userId, userName); + requestBoxes.put(userId, partyRequestBox); + requestBoxPanel.add(partyRequestBox); + requestBoxPanel.revalidate(); + } + + void removeAllRequests() + { + requestBoxes.forEach((key, value) -> requestBoxPanel.remove(value)); + requestBoxPanel.revalidate(); + requestBoxes.clear(); + } + + void removeRequest(String userId) + { + final PartyRequestBox requestBox = requestBoxes.remove(userId); + + if (requestBox != null) + { + requestBoxPanel.remove(requestBox); + requestBoxPanel.revalidate(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPlugin.java index d6c008d40d..5aca5fdcbf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyPlugin.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2019, Tomas Slusny + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,9 +25,11 @@ */ package net.runelite.client.plugins.party; +import com.google.common.base.Strings; import com.google.inject.Binder; import com.google.inject.Provides; import java.awt.Color; +import java.awt.image.BufferedImage; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; @@ -37,18 +40,20 @@ import java.util.UUID; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; +import javax.swing.SwingUtilities; import lombok.Getter; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.KeyCode; -import net.runelite.api.MenuAction; import net.runelite.api.MenuEntry; +import net.runelite.api.Player; import net.runelite.api.Skill; import net.runelite.api.SoundEffectID; import net.runelite.api.Tile; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.CommandExecuted; +import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; import net.runelite.api.events.MenuOptionClicked; import net.runelite.client.callback.ClientThread; @@ -57,21 +62,29 @@ 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.discord.DiscordService; +import net.runelite.client.discord.events.DiscordJoinRequest; import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.OverlayMenuClicked; +import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.PartyChanged; +import net.runelite.client.events.PartyMemberAvatar; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.party.data.PartyData; import net.runelite.client.plugins.party.data.PartyTilePingData; +import net.runelite.client.plugins.party.messages.CharacterNameUpdate; import net.runelite.client.plugins.party.messages.LocationUpdate; import net.runelite.client.plugins.party.messages.SkillUpdate; import net.runelite.client.plugins.party.messages.TilePing; import net.runelite.client.task.Schedule; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.worldmap.WorldMapPoint; import net.runelite.client.ui.overlay.worldmap.WorldMapPointManager; import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.Text; import net.runelite.client.ws.PartyMember; import net.runelite.client.ws.PartyService; import net.runelite.client.ws.WSClient; @@ -81,7 +94,8 @@ import net.runelite.http.api.ws.messages.party.UserSync; @PluginDescriptor( name = "Party", - description = "Shows useful information about current party" + description = "Party management and basic info", + enabledByDefault = false ) public class PartyPlugin extends Plugin { @@ -118,6 +132,12 @@ public class PartyPlugin extends Plugin @Inject private ClientThread clientThread; + @Inject + private ClientToolbar clientToolbar; + + @Inject + private DiscordService discordService; + @Inject @Named("developerMode") boolean developerMode; @@ -128,8 +148,11 @@ public class PartyPlugin extends Plugin @Getter private final List pendingTilePings = Collections.synchronizedList(new ArrayList<>()); + private PartyPanel panel; + private NavigationButton navButton; + private int lastHp, lastPray; - private boolean doSync; + private String lastCharacterName = ""; private boolean sendAlert; @Override @@ -141,17 +164,36 @@ public class PartyPlugin extends Plugin @Override protected void startUp() throws Exception { + panel = injector.getInstance(PartyPanel.class); + + final BufferedImage icon = ImageUtil.loadImageResource(PartyPlugin.class, "panel_icon.png"); + + navButton = NavigationButton.builder() + .tooltip("Party") + .icon(icon) + .priority(9) + .panel(panel) + .build(); + + clientToolbar.addNavigation(navButton); + overlayManager.add(partyStatsOverlay); overlayManager.add(partyPingOverlay); wsClient.registerMessage(SkillUpdate.class); wsClient.registerMessage(TilePing.class); wsClient.registerMessage(LocationUpdate.class); - doSync = true; // Delay sync so eventbus can process correctly. + wsClient.registerMessage(CharacterNameUpdate.class); + // Delay sync so the eventbus can register prior to the sync response + SwingUtilities.invokeLater(this::requestSync); } @Override protected void shutDown() throws Exception { + clientToolbar.removeNavigation(navButton); + + panel = null; + partyDataMap.clear(); pendingTilePings.clear(); worldMapManager.removeIf(PartyWorldMapPoint.class::isInstance); @@ -160,7 +202,7 @@ public class PartyPlugin extends Plugin wsClient.unregisterMessage(SkillUpdate.class); wsClient.unregisterMessage(TilePing.class); wsClient.unregisterMessage(LocationUpdate.class); - doSync = false; + wsClient.unregisterMessage(CharacterNameUpdate.class); sendAlert = false; } @@ -171,28 +213,28 @@ public class PartyPlugin extends Plugin } @Subscribe - public void onOverlayMenuClicked(OverlayMenuClicked event) + public void onConfigChanged(ConfigChanged event) { - if (event.getEntry().getMenuAction() == MenuAction.RUNELITE_OVERLAY && - event.getEntry().getTarget().equals("Party") && - event.getEntry().getOption().equals("Leave")) + if (event.getGroup().equals(PartyConfig.GROUP)) { - party.changeParty(null); + final PartyMember localMember = party.getLocalMember(); - if (!config.messages()) + if (localMember != null) { - return; + if (config.includeSelf()) + { + final PartyData partyData = getPartyData(localMember.getMemberId()); + assert partyData != null; + SwingUtilities.invokeLater(() -> panel.addMember(partyData)); + } + else + { + SwingUtilities.invokeLater(() -> panel.removeMember(localMember.getMemberId())); + } } - final String leaveMessage = new ChatMessageBuilder() - .append(ChatColorType.HIGHLIGHT) - .append("You have left the party.") - .build(); - - chatMessageManager.queue(QueuedMessage.builder() - .type(ChatMessageType.FRIENDSCHATNOTIFICATION) - .runeLiteFormattedMessage(leaveMessage) - .build()); + // rebuild the panel in the event the "Recolor names" option changes + SwingUtilities.invokeLater(panel::updateAll); } } @@ -236,6 +278,35 @@ public class PartyPlugin extends Plugin wsClient.send(tilePing); } + @Subscribe + public void onDiscordJoinRequest(DiscordJoinRequest request) + { + final String requestMessage = new ChatMessageBuilder() + .append(ChatColorType.HIGHLIGHT) + .append("New join request received. Check your Party panel.") + .build(); + + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.FRIENDSCHATNOTIFICATION) + .runeLiteFormattedMessage(requestMessage) + .build()); + + String userName = request.getUsername() + "#" + request.getDiscriminator(); + SwingUtilities.invokeLater(() -> panel.addRequest(request.getUserId(), userName)); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + checkStateChanged(false); + } + + public void replyToRequest(String userId, int reply) + { + discordService.respondToRequest(userId, reply); + panel.removeRequest(userId); + } + @Subscribe public void onTilePing(TilePing event) { @@ -291,41 +362,35 @@ public class PartyPlugin extends Plugin sendInstructionMessage(); } - if (doSync && !party.getMembers().isEmpty()) + checkStateChanged(false); + } + + void requestSync() + { + if (!party.getMembers().isEmpty()) { // Request sync final UserSync userSync = new UserSync(); userSync.setMemberId(party.getLocalMember().getMemberId()); ws.send(userSync); } + } - doSync = false; + @Subscribe + public void onCharacterNameUpdate(final CharacterNameUpdate event) + { + final PartyData partyData = getPartyData(event.getMemberId()); - final int currentHealth = client.getBoostedSkillLevel(Skill.HITPOINTS); - final int currentPrayer = client.getBoostedSkillLevel(Skill.PRAYER); - final int realHealth = client.getRealSkillLevel(Skill.HITPOINTS); - final int realPrayer = client.getRealSkillLevel(Skill.PRAYER); - final PartyMember localMember = party.getLocalMember(); - - if (localMember != null) + if (partyData == null) { - if (currentHealth != lastHp) - { - final SkillUpdate update = new SkillUpdate(Skill.HITPOINTS, currentHealth, realHealth); - update.setMemberId(localMember.getMemberId()); - ws.send(update); - } - - if (currentPrayer != lastPray) - { - final SkillUpdate update = new SkillUpdate(Skill.PRAYER, currentPrayer, realPrayer); - update.setMemberId(localMember.getMemberId()); - ws.send(update); - } + return; } - lastHp = currentHealth; - lastPray = currentPrayer; + String name = event.getCharacterName(); + name = Text.removeTags(Text.toJagexName(name)); + + partyData.setCharacterName(name); + SwingUtilities.invokeLater(() -> panel.updateMember(partyData.getMember().getMemberId())); } @Subscribe @@ -348,6 +413,8 @@ public class PartyPlugin extends Plugin partyData.setPrayer(event.getValue()); partyData.setMaxPrayer(event.getMax()); } + + SwingUtilities.invokeLater(() -> panel.updateMember(partyData.getMember().getMemberId())); } @Subscribe @@ -375,7 +442,7 @@ public class PartyPlugin extends Plugin final String joinMessage = new ChatMessageBuilder() .append(ChatColorType.HIGHLIGHT) - .append(partyData.getName()) + .append(partyData.getMember().getName()) .append(" has joined the party!") .build(); @@ -386,7 +453,7 @@ public class PartyPlugin extends Plugin final PartyMember localMember = party.getLocalMember(); - if (localMember != null && partyData.getMemberId().equals(localMember.getMemberId())) + if (localMember != null && partyData.getMember().getMemberId().equals(localMember.getMemberId())) { sendAlert = true; } @@ -394,6 +461,11 @@ public class PartyPlugin extends Plugin @Subscribe public void onUserSync(final UserSync event) + { + checkStateChanged(true); + } + + private void checkStateChanged(boolean forceSend) { final int currentHealth = client.getBoostedSkillLevel(Skill.HITPOINTS); final int currentPrayer = client.getBoostedSkillLevel(Skill.PRAYER); @@ -401,16 +473,36 @@ public class PartyPlugin extends Plugin final int realPrayer = client.getRealSkillLevel(Skill.PRAYER); final PartyMember localMember = party.getLocalMember(); + final Player localPlayer = client.getLocalPlayer(); + final String characterName = Strings.nullToEmpty(localPlayer != null && client.getGameState().getState() >= GameState.LOADING.getState() ? localPlayer.getName() : null); + if (localMember != null) { - final SkillUpdate hpUpdate = new SkillUpdate(Skill.HITPOINTS, currentHealth, realHealth); - hpUpdate.setMemberId(localMember.getMemberId()); - ws.send(hpUpdate); + if (forceSend || currentHealth != lastHp) + { + final SkillUpdate update = new SkillUpdate(Skill.HITPOINTS, currentHealth, realHealth); + update.setMemberId(localMember.getMemberId()); + ws.send(update); + } - final SkillUpdate prayUpdate = new SkillUpdate(Skill.PRAYER, currentPrayer, realPrayer); - prayUpdate.setMemberId(localMember.getMemberId()); - ws.send(prayUpdate); + if (forceSend || currentPrayer != lastPray) + { + final SkillUpdate update = new SkillUpdate(Skill.PRAYER, currentPrayer, realPrayer); + update.setMemberId(localMember.getMemberId()); + ws.send(update); + } + + if (forceSend || !characterName.equals(lastCharacterName)) + { + final CharacterNameUpdate update = new CharacterNameUpdate(characterName); + update.setMemberId(localMember.getMemberId()); + ws.send(update); + } } + + lastHp = currentHealth; + lastPray = currentPrayer; + lastCharacterName = characterName; } @Subscribe @@ -424,7 +516,7 @@ public class PartyPlugin extends Plugin { final String joinMessage = new ChatMessageBuilder() .append(ChatColorType.HIGHLIGHT) - .append(removed.getName()) + .append(removed.getMember().getName()) .append(" has left the party!") .build(); @@ -435,6 +527,8 @@ public class PartyPlugin extends Plugin } worldMapManager.remove(removed.getWorldMapPoint()); + + SwingUtilities.invokeLater(() -> panel.removeMember(event.getMemberId())); } } @@ -445,6 +539,12 @@ public class PartyPlugin extends Plugin partyDataMap.clear(); pendingTilePings.clear(); worldMapManager.removeIf(PartyWorldMapPoint.class::isInstance); + + SwingUtilities.invokeLater(() -> + { + panel.removeAllMembers(); + panel.removeAllRequests(); + }); } @Subscribe @@ -464,6 +564,12 @@ public class PartyPlugin extends Plugin } } + @Subscribe + public void onPartyMemberAvatar(PartyMemberAvatar event) + { + SwingUtilities.invokeLater(() -> panel.updateMember(event.getMemberId())); + } + @Nullable PartyData getPartyData(final UUID uuid) { @@ -479,18 +585,32 @@ public class PartyPlugin extends Plugin return partyDataMap.computeIfAbsent(uuid, (u) -> { - final String name = memberById.getName(); final WorldMapPoint worldMapPoint = new PartyWorldMapPoint(new WorldPoint(0, 0, 0), memberById); - worldMapPoint.setTooltip(name); + worldMapPoint.setTooltip(memberById.getName()); // When first joining a party, other members can join before getting a join for self PartyMember partyMember = party.getLocalMember(); - if (partyMember == null || !u.equals(partyMember.getMemberId())) + + boolean isSelf = partyMember != null && u.equals(partyMember.getMemberId()); + + if (!isSelf) { worldMapManager.add(worldMapPoint); } - return new PartyData(u, name, worldMapPoint, ColorUtil.fromObject(name)); + PartyData partyData = new PartyData(memberById, worldMapPoint, ColorUtil.fromObject(memberById.getName())); + partyData.setShowOverlay(config.autoOverlay()); + + if (config.includeSelf() || !isSelf) + { + SwingUtilities.invokeLater(() -> panel.addMember(partyData)); + } + else + { + SwingUtilities.invokeLater(panel::updateParty); + } + + return partyData; }); } @@ -498,7 +618,7 @@ public class PartyPlugin extends Plugin { final String helpMessage = new ChatMessageBuilder() .append(ChatColorType.HIGHLIGHT) - .append("To leave party hold SHIFT and right click party stats overlay.") + .append("To leave the party, click \"Leave party\" on the party panel.") .build(); chatMessageManager.queue(QueuedMessage.builder() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyRequestBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyRequestBox.java new file mode 100644 index 0000000000..f36b262285 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyRequestBox.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021, Jonathan Rousseau + * 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.party; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.MouseDragEventForwarder; +import net.runelite.client.ui.components.shadowlabel.JShadowedLabel; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.SwingUtil; +import net.runelite.discord.DiscordRPC; + +class PartyRequestBox extends JPanel +{ + private static final ImageIcon CONFIRM_ICON = new ImageIcon(ImageUtil.loadImageResource(PartyPlugin.class, "confirm_icon.png")); + private static final ImageIcon CONFIRM_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(ImageUtil.bufferedImageFromImage(CONFIRM_ICON.getImage()), 0.54f)); + private static final ImageIcon CANCEL_ICON = new ImageIcon(ImageUtil.loadImageResource(PartyPlugin.class, "cancel_icon.png")); + private static final ImageIcon CANCEL_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(ImageUtil.bufferedImageFromImage(CANCEL_ICON.getImage()), 0.6f)); + + PartyRequestBox(final PartyPlugin plugin, final JComponent panel, String userId, String userName) + { + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(5, 0, 0, 0)); + + /* The box's wrapping container */ + final JPanel container = new JPanel(); + container.setLayout(new BorderLayout()); + container.setBackground(ColorScheme.DARKER_GRAY_COLOR); + container.setBorder(new EmptyBorder(5, 5, 5, 5)); + + JPanel namesPanel = new JPanel(); + namesPanel.setLayout(new DynamicGridLayout(2, 1)); + namesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + namesPanel.setBorder(new EmptyBorder(2, 5, 2, 5)); + + JShadowedLabel nameLabel = new JShadowedLabel(); + nameLabel.setFont(FontManager.getRunescapeSmallFont()); + nameLabel.setForeground(Color.WHITE); + nameLabel.setText(userName); + + JShadowedLabel messageLabel = new JShadowedLabel(); + messageLabel.setFont(FontManager.getRunescapeSmallFont()); + messageLabel.setForeground(Color.WHITE); + messageLabel.setText("Wants to join your party!"); + + namesPanel.add(nameLabel); + namesPanel.add(messageLabel); + + JPanel actionsContainer = new JPanel(new GridLayout(1, 2, 8, 0)); + actionsContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + JButton confirmButton = new JButton(CONFIRM_ICON); + SwingUtil.removeButtonDecorations(confirmButton); + confirmButton.setToolTipText("Invite"); + confirmButton.setRolloverIcon(CONFIRM_HOVER_ICON); + confirmButton.addActionListener(e -> plugin.replyToRequest(userId, DiscordRPC.DISCORD_REPLY_YES)); + confirmButton.setPreferredSize(new Dimension(18, 18)); + + JButton cancelButton = new JButton(CANCEL_ICON); + SwingUtil.removeButtonDecorations(cancelButton); + cancelButton.setToolTipText("Reject"); + cancelButton.setRolloverIcon(CANCEL_HOVER_ICON); + cancelButton.addActionListener(e -> plugin.replyToRequest(userId, DiscordRPC.DISCORD_REPLY_NO)); + cancelButton.setPreferredSize(new Dimension(18, 18)); + + actionsContainer.add(confirmButton); + actionsContainer.add(cancelButton); + + container.add(namesPanel, BorderLayout.WEST); + container.add(actionsContainer, BorderLayout.EAST); + + // forward mouse drag events to parent panel for drag and drop reordering + MouseDragEventForwarder mouseDragEventForwarder = new MouseDragEventForwarder(panel); + container.addMouseListener(mouseDragEventForwarder); + container.addMouseMotionListener(mouseDragEventForwarder); + + add(container, BorderLayout.NORTH); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyStatsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyStatsOverlay.java index 5635646339..18a1ea41d9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyStatsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/PartyStatsOverlay.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2019, Tomas Slusny + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,9 +33,7 @@ import java.awt.Rectangle; import java.util.Map; import java.util.UUID; import javax.inject.Inject; -import net.runelite.api.MenuAction; import net.runelite.client.plugins.party.data.PartyData; -import net.runelite.client.ui.overlay.OverlayMenuEntry; import net.runelite.client.ui.overlay.OverlayPanel; import net.runelite.client.ui.overlay.components.ComponentConstants; import net.runelite.client.ui.overlay.components.PanelComponent; @@ -62,17 +61,11 @@ public class PartyStatsOverlay extends OverlayPanel this.config = config; panelComponent.setBorder(new Rectangle()); panelComponent.setGap(new Point(0, ComponentConstants.STANDARD_BORDER / 2)); - getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Leave", "Party")); } @Override public Dimension render(Graphics2D graphics) { - if (!config.stats()) - { - return null; - } - final Map partyDataMap = plugin.getPartyDataMap(); if (partyDataMap.isEmpty()) { @@ -81,22 +74,14 @@ public class PartyStatsOverlay extends OverlayPanel panelComponent.setBackgroundColor(null); - boolean only1 = plugin.getPartyDataMap().size() == 1; - synchronized (plugin.getPartyDataMap()) { partyDataMap.forEach((k, v) -> { - if (party.getLocalMember() != null && party.getLocalMember().getMemberId().equals(k)) - { - if (only1) - { - panelComponent.getChildren().add(TitleComponent.builder() - .text("No other party members") - .color(Color.RED) - .build()); - } + boolean isSelf = party.getLocalMember() != null && party.getLocalMember().getMemberId().equals(k); + if (!v.isShowOverlay() || (!config.includeSelf() && isSelf)) + { return; } @@ -104,7 +89,7 @@ public class PartyStatsOverlay extends OverlayPanel panel.getChildren().clear(); final TitleComponent name = TitleComponent.builder() - .text(v.getName()) + .text(v.getCharacterName().isEmpty() ? v.getMember().getName() : v.getCharacterName()) .color(config.recolorNames() ? v.getColor() : Color.WHITE) .build(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/data/PartyData.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/data/PartyData.java index 2593c4fe8f..f61a3b8866 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/party/data/PartyData.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/data/PartyData.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2019, Tomas Slusny + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -25,20 +26,19 @@ package net.runelite.client.plugins.party.data; import java.awt.Color; -import java.util.UUID; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import net.runelite.client.ui.overlay.components.PanelComponent; import net.runelite.client.ui.overlay.worldmap.WorldMapPoint; +import net.runelite.client.ws.PartyMember; @Setter @Getter @RequiredArgsConstructor public class PartyData { - private final UUID memberId; - private final String name; + private final PartyMember member; private final WorldMapPoint worldMapPoint; private final PanelComponent panel = new PanelComponent(); private final Color color; @@ -47,4 +47,6 @@ public class PartyData private int maxHitpoints; private int prayer; private int maxPrayer; + private String characterName = ""; + private boolean showOverlay; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/party/messages/CharacterNameUpdate.java b/runelite-client/src/main/java/net/runelite/client/plugins/party/messages/CharacterNameUpdate.java new file mode 100644 index 0000000000..aff6c058c9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/party/messages/CharacterNameUpdate.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, Jonathan Rousseau + * 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.party.messages; + +import lombok.Data; +import net.runelite.http.api.ws.messages.party.PartyMemberMessage; + +@Data +public class CharacterNameUpdate extends PartyMemberMessage +{ + private final String characterName; +} diff --git a/runelite-client/src/main/java/net/runelite/client/ws/PartyService.java b/runelite-client/src/main/java/net/runelite/client/ws/PartyService.java index 4c70a624e6..dc00df2e4d 100644 --- a/runelite-client/src/main/java/net/runelite/client/ws/PartyService.java +++ b/runelite-client/src/main/java/net/runelite/client/ws/PartyService.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Adam + * Copyright (c) 2021, Jonathan Rousseau * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,10 +25,14 @@ */ package net.runelite.client.ws; +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import lombok.Getter; @@ -42,6 +47,7 @@ import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.PartyChanged; import net.runelite.client.util.Text; +import net.runelite.client.events.PartyMemberAvatar; import static net.runelite.client.util.Text.JAGEX_PRINTABLE_CHAR_MATCHER; import net.runelite.http.api.ws.messages.party.Join; import net.runelite.http.api.ws.messages.party.Part; @@ -68,7 +74,10 @@ public class PartyService private UUID localPartyId = UUID.randomUUID(); @Getter - private UUID partyId; + private UUID publicPartyId; // public party id, for advertising on discord, derived from the secret + + @Getter + private UUID partyId; // secret party id @Setter private String username; @@ -83,8 +92,14 @@ public class PartyService eventBus.register(this); } - public void changeParty(UUID newParty) + public void changeParty(@Nullable UUID newParty) { + if (username == null) + { + log.warn("Tried to join a party with no username"); + return; + } + if (wsClient.sessionExists()) { wsClient.send(new Part()); @@ -93,6 +108,8 @@ public class PartyService log.debug("Party change to {}", newParty); members.clear(); partyId = newParty; + // The public party ID needs to be consistent across party members, but not a secret + publicPartyId = newParty != null ? UUID.nameUUIDFromBytes(Hashing.sha256().hashString(newParty.toString(), Charsets.UTF_8).asBytes()) : null; if (partyId == null) { @@ -145,7 +162,7 @@ public class PartyService } } - @Subscribe + @Subscribe(priority = 1) // run prior to plugins so that the member is removed by the time the plugins see it. public void onUserPart(final UserPart message) { members.removeIf(member -> member.getMemberId().equals(message.getMemberId())); @@ -218,6 +235,17 @@ public class PartyService return localPartyId.equals(partyId); } + public void setPartyMemberAvatar(UUID memberID, BufferedImage image) + { + final PartyMember memberById = getMemberById(memberID); + + if (memberById != null) + { + memberById.setAvatar(image); + eventBus.post(new PartyMemberAvatar(memberID, image)); + } + } + private static String cleanUsername(String username) { String s = Text.removeTags(JAGEX_PRINTABLE_CHAR_MATCHER.retainFrom(username)); diff --git a/runelite-client/src/main/java/net/runelite/client/ws/WSClient.java b/runelite-client/src/main/java/net/runelite/client/ws/WSClient.java index 21e7cbafc3..88fc16abed 100644 --- a/runelite-client/src/main/java/net/runelite/client/ws/WSClient.java +++ b/runelite-client/src/main/java/net/runelite/client/ws/WSClient.java @@ -191,7 +191,7 @@ public class WSClient extends WebSocketListener implements AutoCloseable @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { - log.warn("Error in websocket {}:{}", response, t); + log.warn("Error in websocket: {}", response, t); this.webSocket = null; } } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/party/cancel_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/party/cancel_icon.png new file mode 100644 index 0000000000..3f4915d041 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/party/cancel_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/party/confirm_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/party/confirm_icon.png new file mode 100644 index 0000000000..0a60af0872 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/party/confirm_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/party/panel_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/party/panel_icon.png new file mode 100644 index 0000000000..10a86ecd80 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/party/panel_icon.png differ diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java index 815fff0639..dae0188f42 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java @@ -27,9 +27,7 @@ package net.runelite.client.plugins.discord; import com.google.inject.Guice; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; -import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.UUID; import javax.inject.Inject; import javax.inject.Named; import net.runelite.api.Client; @@ -83,7 +81,6 @@ public class DiscordStateTest public void before() { Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); - when(partyService.getLocalPartyId()).thenReturn(UUID.nameUUIDFromBytes("test".getBytes(StandardCharsets.UTF_8))); } @Test