From 31d8e88099588a6d4a8e7ab19e275a1f0fd73c13 Mon Sep 17 00:00:00 2001 From: Ruben Amendoeira Date: Fri, 19 Oct 2018 09:02:16 +0100 Subject: [PATCH] Loot tracker - Add ability to ignore items (#5483) This adds the ability to toggle items in the Loot Tracker, when an item is toggled, its price won't count towards the price check, and if the "Show ignored items" config option is not enabled, it won't be displayed on the loot tracker UI either. Fixes #5060 Fixes #4661 --- .../plugins/loottracker/LootTrackerBox.java | 93 ++++++++++++++++-- .../loottracker/LootTrackerConfig.java | 51 ++++++++++ .../plugins/loottracker/LootTrackerItem.java | 13 ++- .../plugins/loottracker/LootTrackerPanel.java | 85 +++++++++++++++- .../loottracker/LootTrackerPlugin.java | 64 +++++++++++- .../plugins/loottracker/invisible_icon.png | Bin 0 -> 398 bytes .../plugins/loottracker/visible_icon.png | Bin 0 -> 312 bytes 7 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerConfig.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/invisible_icon.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/visible_icon.png diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java index 6ed843e12e..b4ffdcb44e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java @@ -29,23 +29,31 @@ import com.google.common.base.Strings; import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; import javax.annotation.Nullable; +import javax.swing.ImageIcon; import javax.swing.JLabel; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.SwingConstants; import javax.swing.border.EmptyBorder; import lombok.Getter; +import net.runelite.client.game.AsyncBufferedImage; import net.runelite.client.game.ItemManager; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; +import net.runelite.client.util.ImageUtil; import net.runelite.client.util.StackFormatter; class LootTrackerBox extends JPanel { private static final int ITEMS_PER_ROW = 5; + private final JPanel itemContainer = new JPanel(); private final JLabel priceLabel = new JLabel(); private final JLabel subTitleLabel = new JLabel(); @@ -58,10 +66,20 @@ class LootTrackerBox extends JPanel @Getter private long totalPrice; - LootTrackerBox(final ItemManager itemManager, final String id, @Nullable final String subtitle) + private boolean hideIgnoredItems; + private BiConsumer onItemToggle; + + LootTrackerBox( + final ItemManager itemManager, + final String id, + @Nullable final String subtitle, + final boolean hideIgnoredItems, + final BiConsumer onItemToggle) { this.id = id; this.itemManager = itemManager; + this.onItemToggle = onItemToggle; + this.hideIgnoredItems = hideIgnoredItems; setLayout(new BorderLayout(0, 1)); setBorder(new EmptyBorder(5, 0, 0, 0)); @@ -93,13 +111,21 @@ class LootTrackerBox extends JPanel add(itemContainer, BorderLayout.CENTER); } - int getTotalKills() + /** + * Returns total amount of kills, removing ignored kills when necessary + * + * @return total amount of kills + */ + long getTotalKills() { - return records.size(); + return hideIgnoredItems + ? records.stream().filter(r -> !Arrays.stream(r.getItems()).allMatch(LootTrackerItem::isIgnored)).count() + : records.size(); } /** * Checks if this box matches specified record + * * @param record loot record * @return true if match is made */ @@ -110,6 +136,7 @@ class LootTrackerBox extends JPanel /** * Checks if this box matches specified id + * * @param id other record id * @return true if match is made */ @@ -156,13 +183,30 @@ class LootTrackerBox extends JPanel final List items = new ArrayList<>(); totalPrice = 0; - for (LootTrackerRecord records : records) + for (LootTrackerRecord record : records) { - allItems.addAll(Arrays.asList(records.getItems())); + allItems.addAll(Arrays.asList(record.getItems())); + } + + if (hideIgnoredItems) + { + /* If all the items in this box are ignored */ + boolean hideBox = allItems.stream().allMatch(LootTrackerItem::isIgnored); + setVisible(!hideBox); + + if (hideBox) + { + return; + } } for (final LootTrackerItem entry : allItems) { + if (entry.isIgnored() && hideIgnoredItems) + { + continue; + } + totalPrice += entry.getPrice(); int quantity = 0; @@ -175,12 +219,13 @@ class LootTrackerBox extends JPanel break; } } + if (quantity > 0) { int newQuantity = entry.getQuantity() + quantity; long pricePerItem = entry.getPrice() == 0 ? 0 : (entry.getPrice() / entry.getQuantity()); - items.add(new LootTrackerItem(entry.getId(), entry.getName(), newQuantity, pricePerItem * newQuantity)); + items.add(new LootTrackerItem(entry.getId(), entry.getName(), newQuantity, pricePerItem * newQuantity, entry.isIgnored())); } else { @@ -208,8 +253,39 @@ class LootTrackerBox extends JPanel imageLabel.setToolTipText(buildToolTip(item)); imageLabel.setVerticalAlignment(SwingConstants.CENTER); imageLabel.setHorizontalAlignment(SwingConstants.CENTER); - itemManager.getImage(item.getId(), item.getQuantity(), item.getQuantity() > 1).addTo(imageLabel); + + AsyncBufferedImage itemImage = itemManager.getImage(item.getId(), item.getQuantity(), item.getQuantity() > 1); + + if (item.isIgnored()) + { + Runnable addTransparency = () -> + { + BufferedImage transparentImage = ImageUtil.alphaOffset(itemImage, .3f); + imageLabel.setIcon(new ImageIcon(transparentImage)); + }; + itemImage.onChanged(addTransparency); + addTransparency.run(); + } + else + { + itemImage.addTo(imageLabel); + } + slotContainer.add(imageLabel); + + // Create popup menu + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + slotContainer.setComponentPopupMenu(popupMenu); + + final JMenuItem toggle = new JMenuItem("Toggle item"); + toggle.addActionListener(e -> + { + item.setIgnored(!item.isIgnored()); + onItemToggle.accept(item.getName(), item.isIgnored()); + }); + + popupMenu.add(toggle); } itemContainer.add(slotContainer); @@ -223,6 +299,7 @@ class LootTrackerBox extends JPanel final String name = item.getName(); final int quantity = item.getQuantity(); final long price = item.getPrice(); - return name + " x " + quantity + " (" + StackFormatter.quantityToStackSize(price) + ")"; + final String ignoredLabel = item.isIgnored() ? " - Ignored" : ""; + return name + " x " + quantity + " (" + StackFormatter.quantityToStackSize(price) + ") " + ignoredLabel; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerConfig.java new file mode 100644 index 0000000000..5776543b81 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018, Psikoi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.runelite.client.plugins.loottracker; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("loottracker") +public interface LootTrackerConfig extends Config +{ + @ConfigItem( + keyName = "ignoredItems", + name = "Ignored items", + description = "Configures which items should be ignored when calculating loot prices." + ) + default String getIgnoredItems() + { + return ""; + } + + @ConfigItem( + keyName = "ignoredItems", + name = "", + description = "" + ) + void setIgnoredItems(String key); +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerItem.java index 9e3a36ad86..feb1504681 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerItem.java @@ -24,13 +24,22 @@ */ package net.runelite.client.plugins.loottracker; -import lombok.Value; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; -@Value +@AllArgsConstructor class LootTrackerItem { + @Getter private final int id; + @Getter private final String name; + @Getter private final int quantity; + @Getter private final long price; + @Getter + @Setter + private boolean ignored; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java index 2df6499d18..8764ad79c5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java @@ -61,6 +61,10 @@ class LootTrackerPanel extends PluginPanel private static final ImageIcon GROUPED_LOOT_VIEW_HOVER; private static final ImageIcon BACK_ARROW_ICON; private static final ImageIcon BACK_ARROW_ICON_HOVER; + private static final ImageIcon VISIBLE_ICON; + private static final ImageIcon VISIBLE_ICON_HOVER; + private static final ImageIcon INVISIBLE_ICON; + private static final ImageIcon INVISIBLE_ICON_HOVER; private static final String HTML_LABEL_TEMPLATE = "%s%s"; @@ -81,6 +85,7 @@ class LootTrackerPanel extends PluginPanel private final JPanel actionsContainer = new JPanel(); private final JLabel detailsTitle = new JLabel(); private final JLabel backBtn = new JLabel(); + private final JLabel viewHiddenBtn = new JLabel(); private final JLabel singleLootBtn = new JLabel(); private final JLabel groupedLootBtn = new JLabel(); @@ -89,7 +94,10 @@ class LootTrackerPanel extends PluginPanel private final List boxes = new ArrayList<>(); private final ItemManager itemManager; + private final LootTrackerPlugin plugin; + private boolean groupLoot; + private boolean hideIgnoredItems; private String currentView; static @@ -97,6 +105,8 @@ class LootTrackerPanel extends PluginPanel final BufferedImage singleLootImg = ImageUtil.getResourceStreamFromClass(LootTrackerPlugin.class, "single_loot_icon.png"); final BufferedImage groupedLootImg = ImageUtil.getResourceStreamFromClass(LootTrackerPlugin.class, "grouped_loot_icon.png"); final BufferedImage backArrowImg = ImageUtil.getResourceStreamFromClass(LootTrackerPlugin.class, "back_icon.png"); + final BufferedImage visibleImg = ImageUtil.getResourceStreamFromClass(LootTrackerPlugin.class, "visible_icon.png"); + final BufferedImage invisibleImg = ImageUtil.getResourceStreamFromClass(LootTrackerPlugin.class, "invisible_icon.png"); SINGLE_LOOT_VIEW = new ImageIcon(singleLootImg); SINGLE_LOOT_VIEW_FADED = new ImageIcon(ImageUtil.alphaOffset(singleLootImg, -180)); @@ -108,11 +118,20 @@ class LootTrackerPanel extends PluginPanel BACK_ARROW_ICON = new ImageIcon(backArrowImg); BACK_ARROW_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(backArrowImg, -180)); + + VISIBLE_ICON = new ImageIcon(visibleImg); + VISIBLE_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(visibleImg, -220)); + + INVISIBLE_ICON = new ImageIcon(invisibleImg); + INVISIBLE_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(invisibleImg, -220)); } - LootTrackerPanel(final ItemManager itemManager) + LootTrackerPanel(final LootTrackerPlugin plugin, final ItemManager itemManager) { this.itemManager = itemManager; + this.plugin = plugin; + this.hideIgnoredItems = true; + setBorder(new EmptyBorder(6, 6, 6, 6)); setBackground(ColorScheme.DARK_GRAY_COLOR); setLayout(new BorderLayout()); @@ -128,7 +147,7 @@ class LootTrackerPanel extends PluginPanel actionsContainer.setBorder(new EmptyBorder(5, 5, 5, 10)); actionsContainer.setVisible(false); - final JPanel viewControls = new JPanel(new GridLayout(1, 2, 10, 0)); + final JPanel viewControls = new JPanel(new GridLayout(1, 3, 10, 0)); viewControls.setBackground(ColorScheme.DARKER_GRAY_COLOR); singleLootBtn.setIcon(SINGLE_LOOT_VIEW); @@ -177,9 +196,34 @@ class LootTrackerPanel extends PluginPanel } }); + viewHiddenBtn.setIcon(VISIBLE_ICON); + viewHiddenBtn.setToolTipText("Show ignored items"); + viewHiddenBtn.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + changeItemHiding(!hideIgnoredItems); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + viewHiddenBtn.setIcon(hideIgnoredItems ? INVISIBLE_ICON : VISIBLE_ICON); + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + viewHiddenBtn.setIcon(hideIgnoredItems ? INVISIBLE_ICON_HOVER : VISIBLE_ICON_HOVER); + } + }); + viewControls.add(groupedLootBtn); viewControls.add(singleLootBtn); + viewControls.add(viewHiddenBtn); changeGrouping(true); + changeItemHiding(true); final JPanel leftTitleContainer = new JPanel(new BorderLayout(5, 0)); leftTitleContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); @@ -286,6 +330,7 @@ class LootTrackerPanel extends PluginPanel /** * Changes grouping mode of panel + * * @param group if loot should be grouped or not */ private void changeGrouping(boolean group) @@ -296,6 +341,38 @@ class LootTrackerPanel extends PluginPanel singleLootBtn.setIcon(group ? SINGLE_LOOT_VIEW_FADED : SINGLE_LOOT_VIEW); } + /** + * Changes item hiding mode of panel + * + * @param hide if ignored items should be hidden or not + */ + private void changeItemHiding(boolean hide) + { + hideIgnoredItems = hide; + rebuild(); + viewHiddenBtn.setIcon(hideIgnoredItems ? VISIBLE_ICON : INVISIBLE_ICON); + } + + /** + * After an item changed it's ignored state, iterate all the records and make + * sure all items of the same name also get updated + */ + void updateIgnoredRecords() + { + for (LootTrackerRecord r : records) + { + for (LootTrackerItem item : r.getItems()) + { + if (plugin.isIgnored(item.getName()) != item.isIgnored()) + { + item.setIgnored(plugin.isIgnored(item.getName())); + } + } + } + + rebuild(); + } + /** * Rebuilds all the boxes from scratch using existing listed records, depending on the grouping mode. */ @@ -342,7 +419,7 @@ class LootTrackerPanel extends PluginPanel overallPanel.setVisible(true); // Create box - final LootTrackerBox box = new LootTrackerBox(itemManager, record.getTitle(), record.getSubTitle()); + final LootTrackerBox box = new LootTrackerBox(itemManager, record.getTitle(), record.getSubTitle(), hideIgnoredItems, plugin::toggleItem); box.combine(record); // Create popup menu @@ -386,7 +463,7 @@ class LootTrackerPanel extends PluginPanel private void updateOverall() { final long overallGp = boxes.stream().mapToLong(LootTrackerBox::getTotalPrice).sum(); - final int overallKills = boxes.stream().mapToInt(LootTrackerBox::getTotalKills).sum(); + final long overallKills = boxes.stream().mapToLong(LootTrackerBox::getTotalKills).sum(); overallKillsLabel.setText(htmlLabel("Total count: ", overallKills)); overallGpLabel.setText(htmlLabel("Total value: ", overallGp)); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java index 1d7a1befcf..4aa68399c1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java @@ -25,12 +25,17 @@ */ package net.runelite.client.plugins.loottracker; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -47,8 +52,10 @@ import net.runelite.api.Player; import net.runelite.api.SpriteID; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.WidgetLoaded; import net.runelite.api.widgets.WidgetID; +import net.runelite.client.config.ConfigManager; import net.runelite.client.events.NpcLootReceived; import net.runelite.client.events.PlayerLootReceived; import net.runelite.client.game.ItemManager; @@ -74,6 +81,13 @@ public class LootTrackerPlugin extends Plugin private static final Pattern CLUE_SCROLL_PATTERN = Pattern.compile("You have completed [0-9]+ ([a-z]+) Treasure Trails."); private static final int THEATRE_OF_BLOOD_REGION = 12867; + private static final Splitter COMMA_SPLITTER = Splitter + .on(",") + .omitEmptyStrings() + .trimResults(); + + private static final Joiner COMMA_JOINER = Joiner.on(",").skipNulls(); + @Inject private ClientToolbar clientToolbar; @@ -83,6 +97,9 @@ public class LootTrackerPlugin extends Plugin @Inject private SpriteManager spriteManager; + @Inject + private LootTrackerConfig config; + @Inject private Client client; @@ -90,6 +107,8 @@ public class LootTrackerPlugin extends Plugin private NavigationButton navButton; private String eventType; + private List ignoredItems = new ArrayList<>(); + private static Collection stack(Collection items) { final List list = new ArrayList<>(); @@ -119,10 +138,27 @@ public class LootTrackerPlugin extends Plugin return list; } + @Provides + LootTrackerConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(LootTrackerConfig.class); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("loottracker")) + { + ignoredItems = COMMA_SPLITTER.splitToList(config.getIgnoredItems()); + panel.updateIgnoredRecords(); + } + } + @Override protected void startUp() throws Exception { - panel = new LootTrackerPanel(itemManager); + ignoredItems = COMMA_SPLITTER.splitToList(config.getIgnoredItems()); + panel = new LootTrackerPanel(this, itemManager); spriteManager.getSpriteAsync(SpriteID.TAB_INVENTORY, 0, panel::loadHeaderIcon); final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "panel_icon.png"); @@ -253,6 +289,28 @@ public class LootTrackerPlugin extends Plugin } } + void toggleItem(String name, boolean ignore) + { + final Set ignoredItemSet = new HashSet<>(ignoredItems); + + if (ignore) + { + ignoredItemSet.add(name); + } + else + { + ignoredItemSet.remove(name); + } + + config.setIgnoredItems(COMMA_JOINER.join(ignoredItemSet)); + panel.updateIgnoredRecords(); + } + + boolean isIgnored(String name) + { + return ignoredItems.contains(name); + } + private LootTrackerItem[] buildEntries(final Collection itemStacks) { return itemStacks.stream().map(itemStack -> @@ -260,12 +318,14 @@ public class LootTrackerPlugin extends Plugin final ItemComposition itemComposition = itemManager.getItemComposition(itemStack.getId()); final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemStack.getId(); final long price = (long) itemManager.getItemPrice(realItemId) * (long) itemStack.getQuantity(); + final boolean ignored = ignoredItems.contains(itemComposition.getName()); return new LootTrackerItem( itemStack.getId(), itemComposition.getName(), itemStack.getQuantity(), - price); + price, + ignored); }).toArray(LootTrackerItem[]::new); } } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/invisible_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/invisible_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a52cbfe0a76315e75450ecf99b2d4a1ba6cba263 GIT binary patch literal 398 zcmV;90df9`P)pWjvID*%(O= zoHOH+%$A!vUV7YJL9eH#(DuZ-)8-7xH4NKKodQA3cx2$%HFAyg`*4{BtGcSLuHvKI sFS)HQ`)lH2{EYi25{P>A{1bKa2l6jn4>Jw?FaQ7m07*qoM6N<$f+x_c<^TWy literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/visible_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/visible_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5c4232c808f46b2fee47a5ac1f5045792118165b GIT binary patch literal 312 zcmV-80muG{P)PbXFR49?Xk)cilQ4mGX+kl~|Xh=}~0+3ZeLIV6k(}WZpztPGP zWQzlVKp+q#grIQz0O1R&Rv@UN2--ae-tHFG_o|u6N#@QSSgOKe;|51GfG=Ko4)Y4_ zp0~~{m3s0Yxlr}nk!{-vNAFlIkahhYMk_*gN%?OfG2*YfLX z-W3sa>^O|Ky;%R7)Um0HO6}( literal 0 HcmV?d00001