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,
+}