From 443cc7569f4a2815438af8ab75c0114548418d1f Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 1 May 2018 19:00:52 -0500 Subject: [PATCH] xp tracker: rewrite tracking datastructures Fix showing inaccurate "actions left" number #1735 Fix tracker confusing switching accounts with xp gain #1273 --- .../client/plugins/xptracker/XpInfoBox.java | 92 +++++----- .../client/plugins/xptracker/XpPanel.java | 46 +++-- .../plugins/xptracker/XpSnapshotSingle.java | 43 +++++ .../plugins/xptracker/XpSnapshotTotal.java | 39 +++++ .../client/plugins/xptracker/XpState.java | 160 ++++++++++++++++++ .../{SkillXPInfo.java => XpStateSingle.java} | 53 +++--- .../plugins/xptracker/XpStateTotal.java | 55 ++++++ .../plugins/xptracker/XpTrackerPlugin.java | 73 +++++++- .../xptracker/XpTrackerServiceImpl.java | 8 +- .../plugins/xptracker/XpUpdateResult.java | 32 ++++ 10 files changed, 488 insertions(+), 113 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotTotal.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java rename runelite-client/src/main/java/net/runelite/client/plugins/xptracker/{SkillXPInfo.java => XpStateSingle.java} (79%) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateTotal.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java index 2edd218016..01bcdcc0bd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java @@ -49,10 +49,9 @@ import javax.swing.SwingUtilities; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; -import lombok.AccessLevel; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; +import net.runelite.api.Skill; import net.runelite.client.game.SkillIconManager; import net.runelite.client.ui.JShadowedLabel; import net.runelite.client.util.LinkBrowser; @@ -65,8 +64,7 @@ class XpInfoBox extends JPanel private final Client client; private final JPanel panel; - @Getter(AccessLevel.PACKAGE) - private final SkillXPInfo xpInfo; + private final Skill skill; private final JPanel container = new JPanel(); private final JPanel statsPanel = new JPanel(); @@ -77,11 +75,11 @@ class XpInfoBox extends JPanel private final JLabel actionsLeft = new JLabel(); private final JLabel levelLabel = new JShadowedLabel(); - XpInfoBox(Client client, JPanel panel, SkillXPInfo xpInfo, SkillIconManager iconManager) throws IOException + XpInfoBox(XpTrackerPlugin xpTrackerPlugin, Client client, JPanel panel, Skill skill, SkillIconManager iconManager) throws IOException { this.client = client; this.panel = panel; - this.xpInfo = xpInfo; + this.skill = skill; setLayout(new BorderLayout()); setBorder(new CompoundBorder @@ -121,7 +119,7 @@ class XpInfoBox extends JPanel // Create open xp tracker menu final JMenuItem openXpTracker = new JMenuItem("Open XP tracker"); - openXpTracker.addActionListener(e -> LinkBrowser.browse(XpPanel.buildXpTrackerUrl(client.getLocalPlayer(), xpInfo.getSkill()))); + openXpTracker.addActionListener(e -> LinkBrowser.browse(XpPanel.buildXpTrackerUrl(client.getLocalPlayer(), skill))); // Create popup menu final JPopupMenu popupMenu = new JPopupMenu(); @@ -134,14 +132,16 @@ class XpInfoBox extends JPanel iconBarPanel.setOpaque(false); // Create skill/reset icon - final BufferedImage skillImage = iconManager.getSkillImage(xpInfo.getSkill()); + final BufferedImage skillImage = iconManager.getSkillImage(skill); final JButton skillIcon = new JButton(); + skillIcon.putClientProperty(SubstanceSynapse.FLAT_LOOK, Boolean.TRUE); skillIcon.putClientProperty(SubstanceSynapse.BUTTON_NEVER_PAINT_BACKGROUND, Boolean.TRUE); skillIcon.setIcon(new ImageIcon(skillImage)); skillIcon.setRolloverIcon(new ImageIcon(createHoverImage(skillImage))); - skillIcon.setToolTipText("Reset " + xpInfo.getSkill().getName() + " tracker"); - skillIcon.addActionListener(e -> reset()); + + skillIcon.setToolTipText("Reset " + skill.getName() + " tracker"); + skillIcon.addActionListener(e -> xpTrackerPlugin.resetSkillState(skill)); skillIcon.setBounds(ICON_BOUNDS); skillIcon.setOpaque(false); skillIcon.setFocusPainted(false); @@ -201,63 +201,47 @@ class XpInfoBox extends JPanel void reset() { - xpInfo.reset(client.getSkillExperience(xpInfo.getSkill())); container.remove(statsPanel); panel.remove(this); panel.revalidate(); } - void init() + void update(boolean updated, XpSnapshotSingle xpSnapshotSingle) { - if (xpInfo.getStartXp() != -1) - { - return; - } - - xpInfo.setStartXp(client.getSkillExperience(xpInfo.getSkill())); + SwingUtilities.invokeLater(() -> rebuildAsync(updated, xpSnapshotSingle)); } - void update() + private void rebuildAsync(boolean updated, XpSnapshotSingle xpSnapshotSingle) { - if (xpInfo.getStartXp() == -1) + if (updated) { - return; - } - - boolean updated = xpInfo.update(client.getSkillExperience(xpInfo.getSkill())); - - SwingUtilities.invokeLater(() -> - { - if (updated) + if (getParent() != panel) { - if (getParent() != panel) - { - panel.add(this); - panel.revalidate(); - } - - levelLabel.setText(String.valueOf(xpInfo.getLevel())); - xpGained.setText(XpPanel.formatLine(xpInfo.getXpGained(), "xp gained")); - xpLeft.setText(XpPanel.formatLine(xpInfo.getXpRemaining(), "xp left")); - actionsLeft.setText(XpPanel.formatLine(xpInfo.getActionsRemaining(), "actions left")); - - final int progress = xpInfo.getSkillProgress(); - - progressBar.setValue(progress); - progressBar.setBackground(Color.getHSBColor((progress / 100.f) * (120.f / 360.f), 1, 1)); - - progressBar.setToolTipText("" - + XpPanel.formatLine(xpInfo.getActions(), "actions") - + "
" - + XpPanel.formatLine(xpInfo.getActionsHr(), "actions/hr") - + "
" - + xpInfo.getTimeTillLevel() + " till next lvl" - + ""); + panel.add(this); + panel.revalidate(); } - // Always update xp/hr as time always changes - xpHr.setText(XpPanel.formatLine(xpInfo.getXpHr(), "xp/hr")); - }); + levelLabel.setText(String.valueOf(xpSnapshotSingle.getCurrentLevel())); + xpGained.setText(XpPanel.formatLine(xpSnapshotSingle.getXpGainedInSession(), "xp gained")); + xpLeft.setText(XpPanel.formatLine(xpSnapshotSingle.getXpRemainingToGoal(), "xp left")); + actionsLeft.setText(XpPanel.formatLine(xpSnapshotSingle.getActionsRemainingToGoal(), "actions left")); + + final int progress = xpSnapshotSingle.getSkillProgressToGoal(); + + progressBar.setValue(progress); + progressBar.setBackground(Color.getHSBColor((progress / 100.f) * (120.f / 360.f), 1, 1)); + + progressBar.setToolTipText("" + + XpPanel.formatLine(xpSnapshotSingle.getActionsInSession(), "actions") + + "
" + + XpPanel.formatLine(xpSnapshotSingle.getActionsPerHour(), "actions/hr") + + "
" + + xpSnapshotSingle.getTimeTillGoal() + " till next lvl" + + ""); + } + + // Always update xp/hr as time always changes + xpHr.setText(XpPanel.formatLine(xpSnapshotSingle.getXpPerHour(), "xp/hr")); } private static BufferedImage createHoverImage(BufferedImage image) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java index 7e41095bba..1782caba72 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java @@ -29,7 +29,6 @@ import java.awt.GridLayout; import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -70,7 +69,7 @@ class XpPanel extends PluginPanel infoPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); final JButton resetButton = new JButton("Reset All"); - resetButton.addActionListener(e -> resetAllInfoBoxes()); + resetButton.addActionListener(e -> xpTrackerPlugin.resetAndInitState()); final JButton openTrackerButton = new JButton("Open XP tracker"); openTrackerButton.addActionListener(e -> LinkBrowser.browse(buildXpTrackerUrl(client.getLocalPlayer(), Skill.OVERALL))); @@ -98,7 +97,7 @@ class XpPanel extends PluginPanel break; } - infoBoxes.put(skill, new XpInfoBox(client, infoBoxPanel, xpTrackerPlugin.getSkillXpInfo(skill), iconManager)); + infoBoxes.put(skill, new XpInfoBox(xpTrackerPlugin, client, infoBoxPanel, skill, iconManager)); } } catch (IOException e) @@ -130,39 +129,36 @@ class XpPanel extends PluginPanel void resetAllInfoBoxes() { infoBoxes.forEach((skill, xpInfoBox) -> xpInfoBox.reset()); - updateTotal(); } - void updateAllInfoBoxes() + void resetSkill(Skill skill) { - infoBoxes.forEach((skill, xpInfoBox) -> xpInfoBox.update()); - updateTotal(); + XpInfoBox xpInfoBox = infoBoxes.get(skill); + if (xpInfoBox != null) + { + xpInfoBox.reset(); + } } - void updateSkillExperience(Skill skill) + void updateSkillExperience(boolean updated, Skill skill, XpSnapshotSingle xpSnapshotSingle) { final XpInfoBox xpInfoBox = infoBoxes.get(skill); - xpInfoBox.update(); - xpInfoBox.init(); - updateTotal(); + + if (xpInfoBox != null) + { + xpInfoBox.update(updated, xpSnapshotSingle); + } } - private void updateTotal() + void updateTotal(XpSnapshotTotal xpSnapshotTotal) { - final AtomicInteger totalXpGainedVal = new AtomicInteger(); - final AtomicInteger totalXpHrVal = new AtomicInteger(); + SwingUtilities.invokeLater(() -> rebuildAsync(xpSnapshotTotal)); + } - for (XpInfoBox xpInfoBox : infoBoxes.values()) - { - totalXpGainedVal.addAndGet(xpInfoBox.getXpInfo().getXpGained()); - totalXpHrVal.addAndGet(xpInfoBox.getXpInfo().getXpHr()); - } - - SwingUtilities.invokeLater(() -> - { - totalXpGained.setText(formatLine(totalXpGainedVal.get(), "total xp gained")); - totalXpHr.setText(formatLine(totalXpHrVal.get(), "total xp/hr")); - }); + private void rebuildAsync(XpSnapshotTotal xpSnapshotTotal) + { + totalXpGained.setText(formatLine(xpSnapshotTotal.getXpGainedInSession(), "total xp gained")); + totalXpHr.setText(formatLine(xpSnapshotTotal.getXpPerHour(), "total xp/hr")); } static String formatLine(double number, String description) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java new file mode 100644 index 0000000000..350a501f47 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018, Levi + * 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.xptracker; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +class XpSnapshotSingle +{ + private int currentLevel; + private int xpGainedInSession; + private int xpRemainingToGoal; + private int xpPerHour; + private int skillProgressToGoal; + private int actionsInSession; + private int actionsRemainingToGoal; + private int actionsPerHour; + private String timeTillGoal; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotTotal.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotTotal.java new file mode 100644 index 0000000000..f3951214a7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotTotal.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, Levi + * 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.xptracker; + +import lombok.Value; + +@Value +class XpSnapshotTotal +{ + private final int xpGainedInSession; + private final int xpPerHour; + + public static XpSnapshotTotal zero() + { + return new XpSnapshotTotal(0, 0); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java new file mode 100644 index 0000000000..1b0adfb17c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2018, Levi + * 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.xptracker; + +import java.util.EnumMap; +import java.util.Map; +import lombok.NonNull; +import net.runelite.api.Skill; + +/** + * Internal state for the XpTrackerPlugin + * + * Note: This class's operations are not currently synchronized. + * It is intended to be called by the XpTrackerPlugin on the client thread. + */ +class XpState +{ + private final XpStateTotal xpTotal = new XpStateTotal(); + private final Map xpSkills = new EnumMap<>(Skill.class); + + /** + * Destroys all internal state, however any XpSnapshotSingle or XpSnapshotTotal remain unaffected. + */ + void reset() + { + xpTotal.reset(); + xpSkills.clear(); + } + + /** + * Resets a single skill + * @param skill Skill to reset + * @param currentXp Current XP to set to, if unknown set to -1 + */ + void resetSkill(Skill skill, int currentXp) + { + xpSkills.remove(skill); + xpSkills.put(skill, new XpStateSingle(skill, currentXp)); + recalculateTotal(); + } + + /** + * Calculates the total skill changes observed in this session or since the last reset + */ + void recalculateTotal() + { + xpTotal.reset(); + + for (XpStateSingle state : xpSkills.values()) + { + xpTotal.addXpGainedInSession(state.getXpGained()); + xpTotal.addXpPerHour(state.getXpHr()); + } + } + + /** + * Updates a skill with the current known XP. + * When the result of this operation is XpUpdateResult.UPDATED, the UI should be updated accordingly. + * This is to distinguish events that reload all the skill's current values (such as world hopping) + * and also first-login when the skills are not initialized (the start XP will be -1 in this case). + * @param skill Skill to update + * @param currentXp Current known XP for this skill + * @return Whether or not the skill has been initialized, there was no change, or it has been updated + */ + XpUpdateResult updateSkill(Skill skill, int currentXp) + { + XpStateSingle state = getSkill(skill); + + if (state.getStartXp() == -1) + { + if (currentXp > 0) + { + initializeSkill(skill, currentXp); + return XpUpdateResult.INITIALIZED; + } + else + { + return XpUpdateResult.NO_CHANGE; + } + } + else + { + int startXp = state.getStartXp(); + int gainedXp = state.getXpGained(); + + if (startXp + gainedXp > currentXp) + { + // Reinitialize with lesser currentXp, this can happen with negative xp lamps + initializeSkill(skill, currentXp); + return XpUpdateResult.INITIALIZED; + } + else + { + return state.update(currentXp) ? XpUpdateResult.UPDATED : XpUpdateResult.NO_CHANGE; + } + } + } + + /** + * Forcefully initialize a skill with a known start XP from the current XP. + * This is used in resetAndInitState by the plugin. It should not result in showing the XP in the UI. + * @param skill Skill to initialize + * @param currentXp Current known XP for the skill + */ + void initializeSkill(Skill skill, int currentXp) + { + xpSkills.put(skill, new XpStateSingle(skill, currentXp)); + } + + @NonNull + private XpStateSingle getSkill(Skill skill) + { + return xpSkills.computeIfAbsent(skill, (s) -> new XpStateSingle(s, -1)); + } + + /** + * Obtain an immutable snapshot of the provided skill + * intended for use with the UI which operates on another thread + * @param skill Skill to obtain the snapshot for + * @return An immutable snapshot of the specified skill for this session since first login or last reset + */ + @NonNull + XpSnapshotSingle getSkillSnapshot(Skill skill) + { + return getSkill(skill).snapshot(); + } + + /** + * Obtain an immutable snapshot of the provided skill + * intended for use with the UI which operates on another thread + * @return An immutable snapshot of total information for this session since first login or last reset + */ + @NonNull + XpSnapshotTotal getTotalSnapshot() + { + return xpTotal.snapshot(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java similarity index 79% rename from runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java rename to runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java index 50b87b15a7..20fb3a4e2d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/SkillXPInfo.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java @@ -35,17 +35,17 @@ import net.runelite.api.Skill; @Data @Slf4j -class SkillXPInfo +class XpStateSingle { private final Skill skill; + private final int startXp; private Instant skillTimeStart = null; - private int startXp = -1; private int xpGained = 0; private int actions = 0; private int nextLevelExp = 0; private int startLevelExp = 0; private int level = 0; - private boolean initialized = false; + private boolean actionsHistoryInitialized = false; private int[] actionExps = new int[10]; private int actionExpIndex = 0; @@ -82,7 +82,7 @@ class SkillXPInfo int getActionsRemaining() { - if (initialized) + if (actionsHistoryInitialized) { long xpRemaining = getXpRemaining() * actionExps.length; long actionExp = 0; @@ -92,7 +92,14 @@ class SkillXPInfo actionExp += actionExps[i]; } - return Math.toIntExact(xpRemaining / actionExp); + // Let's not divide by zero (or negative) + if (actionExp > 0) + { + // Make sure to account for the very last action at the end + long remainder = xpRemaining % actionExp; + long quotient = xpRemaining / actionExp; + return Math.toIntExact(quotient + (remainder > 0 ? 1 : 0)); + } } return Integer.MAX_VALUE; @@ -116,35 +123,24 @@ class SkillXPInfo return "\u221e"; } - void reset(int currentXp) - { - if (startXp != -1) - { - startXp = currentXp; - } - - xpGained = 0; - actions = 0; - skillTimeStart = null; - } boolean update(int currentXp) { if (startXp == -1) { + log.warn("Attempted to update skill state " + skill + " but was not initialized with current xp"); return false; } int originalXp = xpGained + startXp; + int actionExp = currentXp - originalXp; - if (originalXp >= currentXp) + if (actionExp == 0) { return false; } - int actionExp = currentXp - originalXp; - - if (initialized) + if (actionsHistoryInitialized) { actionExps[actionExpIndex] = actionExp; } @@ -156,7 +152,7 @@ class SkillXPInfo actionExps[i] = actionExp; } - initialized = true; + actionsHistoryInitialized = true; } actionExpIndex = (actionExpIndex + 1) % actionExps.length; @@ -178,4 +174,19 @@ class SkillXPInfo return true; } + + XpSnapshotSingle snapshot() + { + return XpSnapshotSingle.builder() + .currentLevel(getLevel()) + .xpGainedInSession(getXpGained()) + .xpRemainingToGoal(getXpRemaining()) + .xpPerHour(getXpHr()) + .skillProgressToGoal(getSkillProgress()) + .actionsInSession(getActions()) + .actionsRemainingToGoal(getActionsRemaining()) + .actionsPerHour(getActionsHr()) + .timeTillGoal(getTimeTillLevel()) + .build(); + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateTotal.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateTotal.java new file mode 100644 index 0000000000..ad9492981c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateTotal.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018, Levi + * 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.xptracker; + +import lombok.Data; + +@Data +class XpStateTotal +{ + private int xpGainedInSession = 0; + private int xpPerHour = 0; + + void reset() + { + xpGainedInSession = 0; + xpPerHour = 0; + } + + void addXpGainedInSession(int skillXpGainedInSession) + { + xpGainedInSession += skillXpGainedInSession; + } + + void addXpPerHour(int skillXpGainedPerHour) + { + xpPerHour += skillXpGainedPerHour; + } + + XpSnapshotTotal snapshot() + { + return new XpSnapshotTotal(xpGainedInSession, xpPerHour); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java index 27b50b2ace..1a97034673 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2017, Cameron + * Copyright (c) 2018, Levi * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -30,8 +31,6 @@ import com.google.inject.Binder; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ScheduledExecutorService; import javax.imageio.ImageIO; @@ -77,7 +76,7 @@ public class XpTrackerPlugin extends Plugin private NavigationButton navButton; private XpPanel xpPanel; - private final Map xpInfos = new HashMap<>(); + private final XpState xpState = new XpState(); private WorldResult worlds; private XpWorldType lastWorldType; @@ -154,7 +153,7 @@ public class XpTrackerPlugin extends Plugin lastUsername = client.getUsername(); lastWorldType = type; - xpPanel.resetAllInfoBoxes(); + resetState(); } } else if (event.getGameState() == GameState.LOGIN_SCREEN) @@ -213,20 +212,78 @@ public class XpTrackerPlugin extends Plugin return xpType; } - public SkillXPInfo getSkillXpInfo(Skill skill) + /** + * Reset internal state and re-initialize all skills with XP currently cached by the RS client + * This is called by the user manually clicking resetSkillState in the UI. + * It reloads the current skills from the client after resetting internal state. + */ + public void resetAndInitState() { - return xpInfos.computeIfAbsent(skill, SkillXPInfo::new); + resetState(); + + for (Skill skill : Skill.values()) + { + int currentXp = client.getSkillExperience(skill); + xpState.initializeSkill(skill, currentXp); + } } + /** + * Throw out everything, the user has chosen a different account or world type. + * This resets both the internal state and UI elements + */ + public void resetState() + { + xpState.reset(); + xpPanel.resetAllInfoBoxes(); + xpPanel.updateTotal(XpSnapshotTotal.zero()); + } + + /** + * Reset an individual skill with the client's current known state of the skill + * Will also clear the skill from the UI. + * @param skill Skill to reset + */ + public void resetSkillState(Skill skill) + { + int currentXp = client.getSkillExperience(skill); + xpState.resetSkill(skill, currentXp); + xpState.recalculateTotal(); + xpPanel.resetSkill(skill); + xpPanel.updateTotal(xpState.getTotalSnapshot()); + } + + @Subscribe public void onXpChanged(ExperienceChanged event) { - xpPanel.updateSkillExperience(event.getSkill()); + final Skill skill = event.getSkill(); + int currentXp = client.getSkillExperience(skill); + + XpUpdateResult updateResult = xpState.updateSkill(skill, currentXp); + + boolean updated = XpUpdateResult.UPDATED.equals(updateResult); + + xpPanel.updateSkillExperience(updated, skill, xpState.getSkillSnapshot(skill)); + xpState.recalculateTotal(); + xpPanel.updateTotal(xpState.getTotalSnapshot()); } @Subscribe public void onGameTick(GameTick event) { - xpPanel.updateAllInfoBoxes(); + // Rebuild calculated values like xp/hr in panel + for (Skill skill : Skill.values()) + { + xpPanel.updateSkillExperience(false, skill, xpState.getSkillSnapshot(skill)); + } + + xpState.recalculateTotal(); + xpPanel.updateTotal(xpState.getTotalSnapshot()); + } + + public XpSnapshotSingle getSkillSnapshot(Skill skill) + { + return xpState.getSkillSnapshot(skill); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java index 979b49de48..7aa3797bf9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java @@ -42,20 +42,18 @@ class XpTrackerServiceImpl implements XpTrackerService @Override public int getActions(Skill skill) { - SkillXPInfo xpInfo = plugin.getSkillXpInfo(skill); - return xpInfo.getActions(); + return plugin.getSkillSnapshot(skill).getActionsInSession(); } @Override public int getActionsHr(Skill skill) { - SkillXPInfo xpInfo = plugin.getSkillXpInfo(skill); - return xpInfo.getActionsHr(); + return plugin.getSkillSnapshot(skill).getActionsPerHour(); } @Override public int getActionsLeft(Skill skill) { - return plugin.getSkillXpInfo(skill).getActionsRemaining(); + return plugin.getSkillSnapshot(skill).getActionsRemainingToGoal(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java new file mode 100644 index 0000000000..fde5e08470 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Levi + * 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.xptracker; + +enum XpUpdateResult +{ + NO_CHANGE, + INITIALIZED, + UPDATED, +}