From 64abf450d9f72778b5ff1555f3ad04485964143c Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 6 Mar 2022 15:12:33 -0500 Subject: [PATCH] loot tracker: store loot in config Since loot is now aggregated, the data is little enough to store in config. This allows loot to persist between sessions even when not logged in. --- .../runelite/client/config/ConfigManager.java | 23 +- .../plugins/loottracker/ConfigLoot.java | 73 ++++ .../plugins/loottracker/LootTrackerBox.java | 1 + .../loottracker/LootTrackerConfig.java | 26 +- .../plugins/loottracker/LootTrackerPanel.java | 92 +++- .../loottracker/LootTrackerPlugin.java | 405 +++++++++++++++--- .../plugins/loottracker/import_icon.png | Bin 0 -> 420 bytes .../loottracker/LootTrackerPluginTest.java | 5 + 8 files changed, 527 insertions(+), 98 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/loottracker/ConfigLoot.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/import_icon.png diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index add85d3d58..70278e10c5 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -58,6 +58,7 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -410,7 +411,27 @@ public class ConfigManager public List getConfigurationKeys(String prefix) { - return properties.keySet().stream().filter(v -> ((String) v).startsWith(prefix)).map(String.class::cast).collect(Collectors.toList()); + return properties.keySet().stream() + .map(String.class::cast) + .filter(k -> k.startsWith(prefix)) + .collect(Collectors.toList()); + } + + public List getRSProfileConfigurationKeys(String group, String profile, String keyPrefix) + { + if (profile == null) + { + return Collections.emptyList(); + } + + assert profile.startsWith(RSPROFILE_GROUP); + + String prefix = group + "." + profile + "." + keyPrefix; + return properties.keySet().stream() + .map(String.class::cast) + .filter(k -> k.startsWith(prefix)) + .map(k -> splitKey(k)[KEY_SPLITTER_KEY]) + .collect(Collectors.toList()); } public static String getWholeKey(String groupName, String profile, String key) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/ConfigLoot.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/ConfigLoot.java new file mode 100644 index 0000000000..0a26254926 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/ConfigLoot.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.loottracker; + +import java.time.Instant; +import java.util.Arrays; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import net.runelite.http.api.loottracker.LootRecordType; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(of = {"type", "name"}) +class ConfigLoot +{ + LootRecordType type; + String name; + int kills; + Instant first = Instant.now(); + Instant last; + int[] drops; + + ConfigLoot(LootRecordType type, String name) + { + this.type = type; + this.name = name; + this.drops = new int[0]; + } + + void add(int id, int qty) + { + for (int i = 0; i < drops.length; i += 2) + { + if (drops[i] == id) + { + drops[i + 1] += qty; + return; + } + } + + drops = Arrays.copyOf(drops, drops.length + 2); + drops[drops.length - 2] = id; + drops[drops.length - 1] = qty; + } + + int numDrops() + { + return drops.length / 2; + } +} 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 97dd4504df..36d784c182 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 @@ -72,6 +72,7 @@ class LootTrackerBox extends JPanel private final ItemManager itemManager; @Getter(AccessLevel.PACKAGE) private final String id; + @Getter(AccessLevel.PACKAGE) private final LootRecordType lootRecordType; private final LootTrackerPriceType priceType; private final boolean showPriceType; 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 index 983cfb45a9..6e1b9f27ed 100644 --- 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 @@ -30,9 +30,11 @@ import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigSection; -@ConfigGroup("loottracker") +@ConfigGroup(LootTrackerConfig.GROUP) public interface LootTrackerConfig extends Config { + String GROUP = "loottracker"; + @ConfigSection( name = "Ignored Entries", description = "The Ignore items and Ignore groups options", @@ -79,28 +81,6 @@ public interface LootTrackerConfig extends Config return false; } - @ConfigItem( - keyName = "saveLoot", - name = "Submit loot tracker data", - description = "Submit loot tracker data" - ) - default boolean saveLoot() - { - return true; - } - - @ConfigItem( - keyName = "syncPanel", - name = "Synchronize panel contents", - description = "Synchronize your local loot tracker with your server data (requires being signed in).
" + - " This means the panel is filled with portions of your remote data on startup
" + - " and deleting data in the panel also deletes it on the server." - ) - default boolean syncPanel() - { - return true; - } - @ConfigItem( keyName = "ignoredEvents", name = "Ignored Loot Sources", 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 bde380c210..11dfb21abf 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 @@ -52,6 +52,7 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRadioButton; import javax.swing.JToggleButton; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicButtonUI; import javax.swing.plaf.basic.BasicToggleButtonUI; @@ -84,29 +85,31 @@ class LootTrackerPanel extends PluginPanel private static final ImageIcon INVISIBLE_ICON_HOVER; private static final ImageIcon COLLAPSE_ICON; private static final ImageIcon EXPAND_ICON; + private static final ImageIcon IMPORT_ICON; private static final String HTML_LABEL_TEMPLATE = "%s%s"; - private static final String SYNC_RESET_ALL_WARNING_TEXT = - "This will permanently delete the current loot from both the client and the RuneLite website."; - private static final String NO_SYNC_RESET_ALL_WARNING_TEXT = - "This will permanently delete the current loot from the client."; + private static final String RESET_ALL_WARNING_TEXT = + "This will permanently delete all loot."; + private static final String RESET_CURRENT_WARNING_TEXT = + "This will permanently delete \"%s\" loot."; + private static final String RESET_ONE_WARNING_TEXT = + "This will delete one kill."; // When there is no loot, display this private final PluginErrorPanel errorPanel = new PluginErrorPanel(); - // When there is loot, display this. This contains the actions, overall, and log panel. - private final JPanel layoutPanel = new JPanel(); - // Handle loot boxes private final JPanel logsContainer = new JPanel(); // Handle overall session data + private final JPanel overallPanel; private final JLabel overallKillsLabel = new JLabel(); private final JLabel overallGpLabel = new JLabel(); private final JLabel overallIcon = new JLabel(); // Details and navigation + private final JPanel actionsPanel; private final JLabel detailsTitle = new JLabel(); private final JButton backBtn = new JButton(); private final JToggleButton viewHiddenBtn = new JToggleButton(); @@ -114,6 +117,8 @@ class LootTrackerPanel extends PluginPanel private final JRadioButton groupedLootBtn = new JRadioButton(); private final JButton collapseBtn = new JButton(); + private final JPanel importNoticePanel; + // Aggregate of all kills private final List aggregateRecords = new ArrayList<>(); // Individual records for the individual kills this session @@ -158,6 +163,8 @@ class LootTrackerPanel extends PluginPanel COLLAPSE_ICON = new ImageIcon(collapseImg); EXPAND_ICON = new ImageIcon(expandedImg); + + IMPORT_ICON = new ImageIcon(ImageUtil.loadImageResource(LootTrackerPlugin.class, "import_icon.png")); } LootTrackerPanel(final LootTrackerPlugin plugin, final ItemManager itemManager, final LootTrackerConfig config) @@ -172,16 +179,18 @@ class LootTrackerPanel extends PluginPanel setLayout(new BorderLayout()); // Create layout panel for wrapping + final JPanel layoutPanel = new JPanel(); layoutPanel.setLayout(new BoxLayout(layoutPanel, BoxLayout.Y_AXIS)); - layoutPanel.setVisible(false); add(layoutPanel, BorderLayout.NORTH); - final JPanel actionsPanel = buildActionsPanel(); - final JPanel overallPanel = buildOverallPanel(); + actionsPanel = buildActionsPanel(); + overallPanel = buildOverallPanel(); + importNoticePanel = createImportNoticePanel(); // Create loot boxes wrapper logsContainer.setLayout(new BoxLayout(logsContainer, BoxLayout.Y_AXIS)); layoutPanel.add(actionsPanel); + layoutPanel.add(importNoticePanel); layoutPanel.add(overallPanel); layoutPanel.add(logsContainer); @@ -202,6 +211,7 @@ class LootTrackerPanel extends PluginPanel actionsContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); actionsContainer.setPreferredSize(new Dimension(0, 30)); actionsContainer.setBorder(new EmptyBorder(5, 5, 5, 10)); + actionsContainer.setVisible(false); final JPanel viewControls = new JPanel(new GridLayout(1, 3, 10, 0)); viewControls.setBackground(ColorScheme.DARKER_GRAY_COLOR); @@ -287,6 +297,7 @@ class LootTrackerPanel extends PluginPanel )); overallPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); overallPanel.setLayout(new BorderLayout()); + overallPanel.setVisible(false); // Add icon and contents final JPanel overallInfo = new JPanel(); @@ -304,10 +315,8 @@ class LootTrackerPanel extends PluginPanel final JMenuItem reset = new JMenuItem("Reset All"); reset.addActionListener(e -> { - final LootTrackerClient client = plugin.getLootTrackerClient(); - final boolean syncLoot = client.getUuid() != null && config.syncPanel(); final int result = JOptionPane.showOptionDialog(overallPanel, - syncLoot ? SYNC_RESET_ALL_WARNING_TEXT : NO_SYNC_RESET_ALL_WARNING_TEXT, + currentView == null ? RESET_ALL_WARNING_TEXT : String.format(RESET_CURRENT_WARNING_TEXT, currentView), "Are you sure?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[]{"Yes", "No"}, "No"); @@ -325,9 +334,14 @@ class LootTrackerPanel extends PluginPanel logsContainer.repaint(); // Delete all loot, or loot matching the current view - if (syncLoot) + if (currentView != null) { - client.delete(currentView); + assert currentType != null; + plugin.removeLootConfig(currentType, currentView); + } + else + { + plugin.removeAllLoot(); } }); @@ -340,6 +354,30 @@ class LootTrackerPanel extends PluginPanel return overallPanel; } + private JPanel createImportNoticePanel() + { + JPanel panel = new JPanel(); + panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(5, 0, 0, 0, ColorScheme.DARK_GRAY_COLOR), + BorderFactory.createEmptyBorder(8, 10, 8, 10) + )); + panel.setLayout(new BorderLayout()); + + final JLabel importLabel = new JLabel("Missing saved loot? Click the
import button to import it."); + importLabel.setForeground(Color.YELLOW); + panel.add(importLabel, BorderLayout.WEST); + + JButton importButton = new JButton(); + SwingUtil.removeButtonDecorations(importButton); + importButton.setIcon(IMPORT_ICON); + importButton.setToolTipText("Import old loot tracker data to current profile"); + importButton.addActionListener(l -> plugin.importLoot()); + panel.add(importButton, BorderLayout.EAST); + + return panel; + } + void updateCollapseText() { collapseBtn.setSelected(isAllCollapsed()); @@ -389,6 +427,14 @@ class LootTrackerPanel extends PluginPanel } } + /** + * Clear all records in the panel + */ + void clearRecords() + { + aggregateRecords.clear(); + } + /** * Adds a Collection of records to the panel */ @@ -530,7 +576,8 @@ class LootTrackerPanel extends PluginPanel // Show main view remove(errorPanel); - layoutPanel.setVisible(true); + actionsPanel.setVisible(true); + overallPanel.setVisible(true); // Create box final LootTrackerBox box = new LootTrackerBox(itemManager, record.getTitle(), record.getType(), record.getSubTitle(), @@ -571,10 +618,8 @@ class LootTrackerPanel extends PluginPanel final JMenuItem reset = new JMenuItem("Reset"); reset.addActionListener(e -> { - final LootTrackerClient client = plugin.getLootTrackerClient(); - final boolean syncLoot = client.getUuid() != null && config.syncPanel(); final int result = JOptionPane.showOptionDialog(box, - syncLoot ? SYNC_RESET_ALL_WARNING_TEXT : NO_SYNC_RESET_ALL_WARNING_TEXT, + groupLoot ? String.format(RESET_CURRENT_WARNING_TEXT, box.getId()) : RESET_ONE_WARNING_TEXT, "Are you sure?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[]{"Yes", "No"}, "No"); @@ -596,9 +641,9 @@ class LootTrackerPanel extends PluginPanel logsContainer.repaint(); // Without loot being grouped we have no way to identify single kills to be deleted - if (client.getUuid() != null && groupLoot && config.syncPanel()) + if (groupLoot) { - client.delete(box.getId()); + plugin.removeLootConfig(box.getLootRecordType(), box.getId()); } }); @@ -691,4 +736,9 @@ class LootTrackerPanel extends PluginPanel final String valueStr = QuantityFormatter.quantityToStackSize(value); return String.format(HTML_LABEL_TEMPLATE, ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), key, valueStr); } + + void toggleImportNotice(boolean on) + { + SwingUtilities.invokeLater(() -> importNoticePanel.setVisible(on)); + } } 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 9c43c44545..39d2b6bf81 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 @@ -26,6 +26,7 @@ package net.runelite.client.plugins.loottracker; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; @@ -33,9 +34,12 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Multiset; import com.google.common.collect.Multisets; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import com.google.inject.Provides; import java.awt.image.BufferedImage; import java.io.IOException; +import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -43,6 +47,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -55,6 +60,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; +import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import lombok.AccessLevel; import lombok.Getter; @@ -96,6 +102,7 @@ import net.runelite.client.events.ClientShutdown; import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.NpcLootReceived; import net.runelite.client.events.PlayerLootReceived; +import net.runelite.client.events.RuneScapeProfileChanged; import net.runelite.client.events.SessionClose; import net.runelite.client.events.SessionOpen; import net.runelite.client.game.ItemManager; @@ -119,12 +126,14 @@ import org.apache.commons.text.WordUtils; @PluginDescriptor( name = "Loot Tracker", description = "Tracks loot from monsters and minigames", - tags = {"drops"}, - enabledByDefault = false + tags = {"drops"} ) @Slf4j public class LootTrackerPlugin extends Plugin { + private static final int MAX_DROPS = 1024; + private static final Duration MAX_AGE = Duration.ofDays(365L); + // Activity/Event loot handling 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; @@ -291,6 +300,12 @@ public class LootTrackerPlugin extends Plugin @Inject private LootManager lootManager; + @Inject + private ConfigManager configManager; + + @Inject + private Gson gson; + private LootTrackerPanel panel; private NavigationButton navButton; @VisibleForTesting @@ -310,6 +325,8 @@ public class LootTrackerPlugin extends Plugin @Inject private LootTrackerClient lootTrackerClient; private final List queuedLoots = new ArrayList<>(); + private String profileKey; + private Instant lastLootImport = Instant.now().minus(1, ChronoUnit.MINUTES); private static Collection stack(Collection items) { @@ -367,20 +384,120 @@ public class LootTrackerPlugin extends Plugin lootTrackerClient.setUuid(null); } + @Subscribe + public void onRuneScapeProfileChanged(RuneScapeProfileChanged e) + { + final String profileKey = configManager.getRSProfileKey(); + if (profileKey == null) + { + return; + } + + if (profileKey.equals(this.profileKey)) + { + return; + } + + log.debug("Profile changed to {}", profileKey); + switchProfile(profileKey); + } + + private void switchProfile(String profileKey) + { + executor.execute(() -> + { + // Current queued loot is for the previous profile, so save it first with the current profile key + submitLoot(); + + this.profileKey = profileKey; + + log.debug("Switched to profile {}", profileKey); + + int drops = 0; + List loots = new ArrayList<>(); + Instant old = Instant.now().minus(MAX_AGE); + for (String key : configManager.getRSProfileConfigurationKeys(LootTrackerConfig.GROUP, profileKey, "drops_")) + { + String json = configManager.getConfiguration(LootTrackerConfig.GROUP, profileKey, key); + ConfigLoot configLoot; + + try + { + configLoot = gson.fromJson(json, ConfigLoot.class); + } + catch (JsonSyntaxException ex) + { + log.warn("Removing loot with malformed json: {}", json, ex); + configManager.unsetConfiguration(LootTrackerConfig.GROUP, profileKey, key); + continue; + } + + if (configLoot.last.isBefore(old)) + { + log.debug("Removing old loot for {} {}", configLoot.type, configLoot.name); + configManager.unsetConfiguration(LootTrackerConfig.GROUP, profileKey, key); + continue; + } + + if (drops >= MAX_DROPS && !loots.isEmpty() && loots.get(0).last.isAfter(configLoot.last)) + { + // fast drop + continue; + } + + sortedInsert(loots, configLoot, Comparator.comparing(ConfigLoot::getLast)); + drops += configLoot.numDrops(); + + if (drops >= MAX_DROPS) + { + ConfigLoot top = loots.remove(0); + drops -= top.numDrops(); + } + } + + log.debug("Loaded {} records", loots.size()); + + clientThread.invokeLater(() -> + { + // convertToLootTrackerRecord must be called on client thread + List records = loots.stream() + .map(this::convertToLootTrackerRecord) + .collect(Collectors.toList()); + SwingUtilities.invokeLater(() -> + { + panel.clearRecords(); + panel.addRecords(records); + }); + }); + + panel.toggleImportNotice(!hasImported()); + }); + } + + private static void sortedInsert(List list, T value, Comparator c) + { + int idx = Collections.binarySearch(list, value, c); + list.add(idx < 0 ? -idx - 1 : idx, value); + } + @Subscribe public void onConfigChanged(ConfigChanged event) { - if (event.getGroup().equals("loottracker")) + if (event.getGroup().equals(LootTrackerConfig.GROUP)) { - ignoredItems = Text.fromCSV(config.getIgnoredItems()); - ignoredEvents = Text.fromCSV(config.getIgnoredEvents()); - SwingUtilities.invokeLater(panel::updateIgnoredRecords); + if ("ignoredItems".equals(event.getKey()) || "ignoredEvents".equals(event.getKey())) + { + ignoredItems = Text.fromCSV(config.getIgnoredItems()); + ignoredEvents = Text.fromCSV(config.getIgnoredEvents()); + SwingUtilities.invokeLater(panel::updateIgnoredRecords); + } } } @Override protected void startUp() throws Exception { + profileKey = null; ignoredItems = Text.fromCSV(config.getIgnoredItems()); ignoredEvents = Text.fromCSV(config.getIgnoredEvents()); panel = new LootTrackerPanel(this, itemManager, config); @@ -401,44 +518,12 @@ public class LootTrackerPlugin extends Plugin if (accountSession != null) { lootTrackerClient.setUuid(accountSession.getUuid()); + } - clientThread.invokeLater(() -> - { - switch (client.getGameState()) - { - case STARTING: - case UNKNOWN: - return false; - } - - executor.submit(() -> - { - if (!config.syncPanel()) - { - return; - } - - Collection lootRecords; - try - { - lootRecords = lootTrackerClient.get(); - } - catch (IOException e) - { - log.debug("Unable to look up loot", e); - return; - } - - log.debug("Loaded {} data entries", lootRecords.size()); - - clientThread.invokeLater(() -> - { - Collection records = convertToLootTrackerRecord(lootRecords); - SwingUtilities.invokeLater(() -> panel.addRecords(records)); - }); - }); - return true; - }); + String profileKey = configManager.getRSProfileKey(); + if (profileKey != null) + { + switchProfile(profileKey); } } @@ -475,13 +560,10 @@ public class LootTrackerPlugin extends Plugin final LootTrackerItem[] entries = buildEntries(stack(items)); SwingUtilities.invokeLater(() -> panel.add(name, type, combatLevel, entries)); - if (config.saveLoot()) + LootRecord lootRecord = new LootRecord(name, type, metadata, toGameItems(items), Instant.now(), getLootWorldId()); + synchronized (queuedLoots) { - LootRecord lootRecord = new LootRecord(name, type, metadata, toGameItems(items), Instant.now(), getLootWorldId()); - synchronized (queuedLoots) - { - queuedLoots.add(lootRecord); - } + queuedLoots.add(lootRecord); } eventBus.post(new LootReceived(name, combatLevel, type, items)); @@ -932,16 +1014,53 @@ public class LootTrackerPlugin extends Plugin queuedLoots.clear(); } - if (!config.saveLoot()) - { - return null; - } + saveLoot(copy); log.debug("Submitting {} loot records", copy.size()); return lootTrackerClient.submit(copy); } + private Collection combine(List records) + { + Map map = new HashMap<>(); + for (LootRecord record : records) + { + ConfigLoot key = new ConfigLoot(record.getType(), record.getEventId()); + ConfigLoot loot = map.computeIfAbsent(key, k -> key); + loot.kills++; + for (GameItem item : record.getDrops()) + { + loot.add(item.getId(), item.getQty()); + } + } + return map.values(); + } + + private void saveLoot(List records) + { + Instant now = Instant.now(); + Collection combinedRecords = combine(records); + for (ConfigLoot record : combinedRecords) + { + ConfigLoot lootConfig = getLootConfig(record.type, record.name); + if (lootConfig == null) + { + lootConfig = record; + } + else + { + lootConfig.kills += record.kills; + for (int i = 0; i < record.drops.length; i += 2) + { + lootConfig.add(record.drops[i], record.drops[i + 1]); + } + } + lootConfig.last = now; + setLootConfig(lootConfig.type, lootConfig.name, lootConfig); + } + } + private void setEvent(LootRecordType lootRecordType, String eventType, Object metadata) { this.lootRecordType = lootRecordType; @@ -1113,6 +1232,18 @@ public class LootTrackerPlugin extends Plugin .collect(Collectors.toCollection(ArrayList::new)); } + private LootTrackerRecord convertToLootTrackerRecord(final ConfigLoot configLoot) + { + LootTrackerItem[] items = new LootTrackerItem[configLoot.drops.length / 2]; + for (int i = 0; i < configLoot.drops.length; i += 2) + { + int id = configLoot.drops[i]; + int qty = configLoot.drops[i + 1]; + items[i >> 1] = buildLootTrackerItem(id, qty); + } + return new LootTrackerRecord(configLoot.name, "", configLoot.type, items, configLoot.kills); + } + /** * Is player currently within the provided map regions */ @@ -1152,4 +1283,172 @@ public class LootTrackerPlugin extends Plugin .runeLiteFormattedMessage(message) .build()); } + + ConfigLoot getLootConfig(LootRecordType type, String name) + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + log.debug("Trying to get loot with no profile!"); + return null; + } + + String json = configManager.getConfiguration(LootTrackerConfig.GROUP, profile, "drops_" + type + "_" + name); + if (json == null) + { + return null; + } + + return gson.fromJson(json, ConfigLoot.class); + } + + void setLootConfig(LootRecordType type, String name, ConfigLoot loot) + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + log.debug("Trying to set loot with no profile!"); + return; + } + + String json = gson.toJson(loot); + configManager.setConfiguration(LootTrackerConfig.GROUP, profile, "drops_" + type + "_" + name, json); + } + + void removeLootConfig(LootRecordType type, String name) + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + log.debug("Trying to remove loot with no profile!"); + return; + } + + configManager.unsetConfiguration(LootTrackerConfig.GROUP, profile, "drops_" + type + "_" + name); + } + + void removeAllLoot() + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + log.debug("Trying to clear loot with no profile!"); + return; + } + + for (String key : configManager.getRSProfileConfigurationKeys(LootTrackerConfig.GROUP, profile, "drops_")) + { + configManager.unsetConfiguration(LootTrackerConfig.GROUP, profile, key); + } + + clearImported(); + panel.toggleImportNotice(true); + } + + void importLoot() + { + if (configManager.getRSProfileKey() == null) + { + JOptionPane.showMessageDialog(panel, "You do not have an active profile to import loot into; log in to the game first."); + return; + } + + if (lootTrackerClient.getUuid() == null) + { + JOptionPane.showMessageDialog(panel, "You are not logged into RuneLite, so loot can not be imported from your account. Log in first."); + return; + } + + if (lastLootImport.isAfter(Instant.now().minus(1, ChronoUnit.MINUTES))) + { + JOptionPane.showMessageDialog(panel, "You imported too recently. Wait a minute and try again."); + return; + } + + lastLootImport = Instant.now(); + + executor.execute(() -> + { + if (hasImported()) + { + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(panel, "You already have imported loot.")); + return; + } + + Collection lootRecords; + try + { + lootRecords = lootTrackerClient.get(); + } + catch (IOException e) + { + log.debug("Unable to look up loot", e); + return; + } + + log.debug("Loaded {} data entries", lootRecords.size()); + + for (LootAggregate record : lootRecords) + { + ConfigLoot lootConfig = getLootConfig(record.getType(), record.getEventId()); + if (lootConfig == null) + { + lootConfig = new ConfigLoot(record.getType(), record.getEventId()); + } + lootConfig.first = record.getFirst_time(); + lootConfig.last = record.getLast_time(); + lootConfig.kills += record.getAmount(); + for (GameItem gameItem : record.getDrops()) + { + lootConfig.add(gameItem.getId(), gameItem.getQty()); + } + setLootConfig(record.getType(), record.getEventId(), lootConfig); + } + + clientThread.invokeLater(() -> + { + Collection records = convertToLootTrackerRecord(lootRecords); + SwingUtilities.invokeLater(() -> panel.addRecords(records)); + }); + + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(panel, "Imported " + lootRecords.size() + " loot entries.")); + + setHasImported(); + panel.toggleImportNotice(false); + }); + } + + void setHasImported() + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + return; + } + + configManager.setConfiguration(LootTrackerConfig.GROUP, profile, "imported", 1); + } + + boolean hasImported() + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + return false; + } + + Integer i = configManager.getConfiguration(LootTrackerConfig.GROUP, profile, "imported", Integer.class); + return i != null && i == 1; + } + + void clearImported() + { + String profile = profileKey; + if (Strings.isNullOrEmpty(profile)) + { + return; + } + + configManager.unsetConfiguration(LootTrackerConfig.GROUP, profile, "imported"); + } } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/import_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/import_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5402cd593e9b7c02a90b5551d91bb9c19f3428a5 GIT binary patch literal 420 zcmV;V0bBlwP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA0Xa!TK~y+TwbQ#w zLqQP6@!gn!pxDI9Mr>_tZF~wveFDMOMlA#p1hGh?l~~$oZ6gvdq!6@HlS1$b6l}Ec zcKm;v899*1vCt2GFgv?v&V?lWd$KHZ70lrn4|u{A<`I$36~+z@;2g>=cCn478l(>0 zuz08)+QS