xp tracker: rewrite tracking datastructures

Fix showing inaccurate "actions left" number #1735 
Fix tracker confusing switching accounts with xp gain #1273
This commit is contained in:
Levi
2018-05-01 19:00:52 -05:00
committed by Adam
parent a66c3fc6e7
commit 443cc7569f
10 changed files with 488 additions and 113 deletions

View File

@@ -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("<html>"
+ XpPanel.formatLine(xpInfo.getActions(), "actions")
+ "<br/>"
+ XpPanel.formatLine(xpInfo.getActionsHr(), "actions/hr")
+ "<br/>"
+ xpInfo.getTimeTillLevel() + " till next lvl"
+ "</html>");
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("<html>"
+ XpPanel.formatLine(xpSnapshotSingle.getActionsInSession(), "actions")
+ "<br/>"
+ XpPanel.formatLine(xpSnapshotSingle.getActionsPerHour(), "actions/hr")
+ "<br/>"
+ xpSnapshotSingle.getTimeTillGoal() + " till next lvl"
+ "</html>");
}
// Always update xp/hr as time always changes
xpHr.setText(XpPanel.formatLine(xpSnapshotSingle.getXpPerHour(), "xp/hr"));
}
private static BufferedImage createHoverImage(BufferedImage image)

View File

@@ -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)

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2018, Levi <me@levischuck.com>
* 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;
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2018, Levi <me@levischuck.com>
* 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);
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (c) 2018, Levi <me@levischuck.com>
* 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<Skill, XpStateSingle> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018, Levi <me@levischuck.com>
* 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);
}
}

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2017, Cameron <moberg@tuta.io>
* Copyright (c) 2018, Levi <me@levischuck.com>
* 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<Skill, SkillXPInfo> 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);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018, Levi <me@levischuck.com>
* 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,
}