From aa8d2e5b1fb5b679f1e7eb05e2cb251c89ef3cee Mon Sep 17 00:00:00 2001 From: Levi Schuck Date: Wed, 11 Jul 2018 20:10:57 -0500 Subject: [PATCH 1/2] Add dimming UI to progress bar --- .../client/ui/components/DimmableJPanel.java | 90 +++++++++++++++++++ .../client/ui/components/ProgressBar.java | 41 +++++++-- 2 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/DimmableJPanel.java diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/DimmableJPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/DimmableJPanel.java new file mode 100644 index 0000000000..c36dd72abe --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/DimmableJPanel.java @@ -0,0 +1,90 @@ +/* + * 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.ui.components; + +import java.awt.Color; +import javax.swing.JPanel; +import lombok.Getter; + +public class DimmableJPanel extends JPanel +{ + // Dimming state, allows for restoring original colors before dimming + @Getter + private boolean dimmed = false; + private Color dimmedForeground = null; + private Color dimmedBackground = null; + private Color undimmedForeground = null; + private Color undimmedBackground = null; + + @Override + public void setForeground(Color color) + { + undimmedForeground = color; + dimmedForeground = color.darker(); + super.setForeground(color); + } + + @Override + public void setBackground(Color color) + { + undimmedBackground = color; + dimmedBackground = color.darker(); + super.setBackground(color); + } + + @Override + public Color getForeground() + { + return dimmed ? dimmedForeground : undimmedForeground; + } + + @Override + public Color getBackground() + { + return dimmed ? dimmedBackground : undimmedBackground; + } + + /** + * Dimming sets all parts of this component with darker colors except for the central label + * This is useful for showing that progress is paused + * Setting dim to false will restore the original colors from before the component was dimmed. + * @param dimmed + */ + public void setDimmed(boolean dimmed) + { + this.dimmed = dimmed; + + if (dimmed) + { + super.setBackground(dimmedBackground); + super.setForeground(dimmedForeground); + } + else + { + super.setBackground(undimmedBackground); + super.setForeground(undimmedForeground); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/ProgressBar.java b/runelite-client/src/main/java/net/runelite/client/ui/components/ProgressBar.java index c94845ee1e..0c36df1161 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/components/ProgressBar.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/ProgressBar.java @@ -29,7 +29,6 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import javax.swing.JLabel; -import javax.swing.JPanel; import javax.swing.SwingConstants; import javax.swing.border.EmptyBorder; import lombok.Setter; @@ -39,7 +38,7 @@ import net.runelite.client.ui.components.shadowlabel.JShadowedLabel; /** * A progress bar to be displayed underneath the GE offer item panels */ -public class ProgressBar extends JPanel +public class ProgressBar extends DimmableJPanel { @Setter private int maximumValue; @@ -50,11 +49,16 @@ public class ProgressBar extends JPanel private final JLabel leftLabel = new JShadowedLabel(); private final JLabel rightLabel = new JShadowedLabel(); private final JLabel centerLabel = new JShadowedLabel(); + private String centerLabelText = ""; + private String dimmedText = ""; public ProgressBar() { setLayout(new BorderLayout()); + // The background color should be overridden setBackground(Color.GREEN.darker()); + setForeground(Color.GREEN.brighter()); + setPreferredSize(new Dimension(100, 16)); leftLabel.setFont(FontManager.getRunescapeSmallFont()); @@ -70,10 +74,10 @@ public class ProgressBar extends JPanel centerLabel.setHorizontalAlignment(SwingConstants.CENTER); centerLabel.setBorder(new EmptyBorder(2, 0, 0, 0)); + // Adds components to be automatically redrawn when paintComponents is called add(leftLabel, BorderLayout.WEST); add(centerLabel, BorderLayout.CENTER); add(rightLabel, BorderLayout.EAST); - } @Override @@ -88,20 +92,45 @@ public class ProgressBar extends JPanel super.paintComponents(g); } + @Override + public void setDimmed(boolean dimmed) + { + super.setDimmed(dimmed); + + if (dimmed) + { + leftLabel.setForeground(Color.GRAY); + rightLabel.setForeground(Color.GRAY); + centerLabel.setText(dimmedText); + } + else + { + leftLabel.setForeground(Color.WHITE); + rightLabel.setForeground(Color.WHITE); + centerLabel.setText(centerLabelText); + } + } public void setLeftLabel(String txt) { - this.leftLabel.setText(txt); + leftLabel.setText(txt); } public void setRightLabel(String txt) { - this.rightLabel.setText(txt); + rightLabel.setText(txt); } public void setCenterLabel(String txt) { - this.centerLabel.setText(txt); + centerLabelText = txt; + centerLabel.setText(isDimmed() ? dimmedText : txt); + } + + public void setDimmedText(String txt) + { + dimmedText = txt; + centerLabel.setText(isDimmed() ? txt : centerLabelText); } public double getPercentage() From f0e35fcd6da13128cc1987faf2719e4b83076d09 Mon Sep 17 00:00:00 2001 From: Levi Schuck Date: Wed, 11 Jul 2018 20:11:56 -0500 Subject: [PATCH 2/2] Add feature to pause skill timers on logout or after idle period --- .../client/plugins/xptracker/XpInfoBox.java | 63 ++++++++--- .../client/plugins/xptracker/XpPanel.java | 4 +- .../plugins/xptracker/XpPauseState.java | 104 ++++++++++++++++++ .../plugins/xptracker/XpPauseStateSingle.java | 99 +++++++++++++++++ .../client/plugins/xptracker/XpState.java | 5 + .../plugins/xptracker/XpStateSingle.java | 21 ++-- .../plugins/xptracker/XpTrackerConfig.java | 55 +++++++++ .../plugins/xptracker/XpTrackerPlugin.java | 85 ++++++++++++-- 8 files changed, 395 insertions(+), 41 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.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 8f0390b927..d41abdb55b 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 @@ -54,6 +54,15 @@ import net.runelite.client.util.SwingUtil; @Slf4j class XpInfoBox extends JPanel { + // Templates + private static final String HTML_TOOL_TIP_TEMPLATE = + "%s actions done
" + + "%s actions/hr
" + + "%s till goal lvl"; + private static final String HTML_LABEL_TEMPLATE = + "%s%s"; + + // Instance members private final JPanel panel; @Getter(AccessLevel.PACKAGE) @@ -74,6 +83,9 @@ class XpInfoBox extends JPanel private final JLabel expHour = new JLabel(); private final JLabel expLeft = new JLabel(); private final JLabel actionsLeft = new JLabel(); + private final JMenuItem pauseSkill = new JMenuItem("Pause"); + + private boolean paused = false; XpInfoBox(XpTrackerPlugin xpTrackerPlugin, Client client, JPanel panel, Skill skill, SkillIconManager iconManager) throws IOException { @@ -98,12 +110,16 @@ class XpInfoBox extends JPanel final JMenuItem resetOthers = new JMenuItem("Reset others"); resetOthers.addActionListener(e -> xpTrackerPlugin.resetOtherSkillState(skill)); + // Create reset others menu + pauseSkill.addActionListener(e -> xpTrackerPlugin.pauseSkill(skill, !paused)); + // Create popup menu final JPopupMenu popupMenu = new JPopupMenu(); popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); popupMenu.add(openXpTracker); popupMenu.add(reset); popupMenu.add(resetOthers); + popupMenu.add(pauseSkill); JLabel skillIcon = new JLabel(new ImageIcon(iconManager.getSkillImage(skill))); skillIcon.setHorizontalAlignment(SwingConstants.CENTER); @@ -138,6 +154,7 @@ class XpInfoBox extends JPanel progressBar.setMaximumValue(100); progressBar.setBackground(new Color(61, 56, 49)); progressBar.setForeground(SkillColor.values()[skill.ordinal()].getColor()); + progressBar.setDimmedText("Paused"); progressWrapper.add(progressBar, BorderLayout.NORTH); @@ -157,12 +174,12 @@ class XpInfoBox extends JPanel panel.revalidate(); } - void update(boolean updated, XpSnapshotSingle xpSnapshotSingle) + void update(boolean updated, boolean paused, XpSnapshotSingle xpSnapshotSingle) { - SwingUtilities.invokeLater(() -> rebuildAsync(updated, xpSnapshotSingle)); + SwingUtilities.invokeLater(() -> rebuildAsync(updated, paused, xpSnapshotSingle)); } - private void rebuildAsync(boolean updated, XpSnapshotSingle xpSnapshotSingle) + private void rebuildAsync(boolean updated, boolean skillPaused, XpSnapshotSingle xpSnapshotSingle) { if (updated) { @@ -172,6 +189,8 @@ class XpInfoBox extends JPanel panel.revalidate(); } + paused = skillPaused; + // Update information labels expGained.setText(htmlLabel("XP Gained: ", xpSnapshotSingle.getXpGainedInSession())); expLeft.setText(htmlLabel("XP Left: ", xpSnapshotSingle.getXpRemainingToGoal())); @@ -181,28 +200,42 @@ class XpInfoBox extends JPanel progressBar.setValue(xpSnapshotSingle.getSkillProgressToGoal()); progressBar.setCenterLabel(xpSnapshotSingle.getSkillProgressToGoal() + "%"); progressBar.setLeftLabel("Lvl. " + xpSnapshotSingle.getStartLevel()); - progressBar.setRightLabel("Lvl. " + (xpSnapshotSingle.getEndLevel())); + progressBar.setRightLabel("Lvl. " + xpSnapshotSingle.getEndLevel()); - progressBar.setToolTipText("" - + xpSnapshotSingle.getActionsInSession() + " actions done" - + "
" - + xpSnapshotSingle.getActionsPerHour() + " actions/hr" - + "
" - + xpSnapshotSingle.getTimeTillGoal() + " till goal lvl" - + ""); + progressBar.setToolTipText(String.format( + HTML_TOOL_TIP_TEMPLATE, + xpSnapshotSingle.getActionsInSession(), + xpSnapshotSingle.getActionsPerHour(), + xpSnapshotSingle.getTimeTillGoal())); + + progressBar.setDimmed(skillPaused); progressBar.repaint(); } + else if (!paused && skillPaused) + { + // React to the skill state now being paused + progressBar.setDimmed(true); + progressBar.repaint(); + paused = true; + pauseSkill.setText("Unpause"); + } + else if (paused && !skillPaused) + { + // React to the skill being unpaused (without update) + progressBar.setDimmed(false); + progressBar.repaint(); + paused = false; + pauseSkill.setText("Pause"); + } - // Update exp per hour seperately, everytime (not only when there's an update) + // Update exp per hour separately, every time (not only when there's an update) expHour.setText(htmlLabel("XP/Hour: ", xpSnapshotSingle.getXpPerHour())); } static String htmlLabel(String key, int value) { String valueStr = StackFormatter.quantityToRSDecimalStack(value); - - return "" + key + "" + valueStr + ""; + return String.format(HTML_LABEL_TEMPLATE, SwingUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), key, valueStr); } - } 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 5a87715b13..4226e4009d 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 @@ -172,13 +172,13 @@ class XpPanel extends PluginPanel } } - void updateSkillExperience(boolean updated, Skill skill, XpSnapshotSingle xpSnapshotSingle) + void updateSkillExperience(boolean updated, boolean paused, Skill skill, XpSnapshotSingle xpSnapshotSingle) { final XpInfoBox xpInfoBox = infoBoxes.get(skill); if (xpInfoBox != null) { - xpInfoBox.update(updated, xpSnapshotSingle); + xpInfoBox.update(updated, paused, xpSnapshotSingle); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java new file mode 100644 index 0000000000..9f57de71c3 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java @@ -0,0 +1,104 @@ +/* + * 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 net.runelite.api.Skill; + +class XpPauseState +{ + // Internal state + private final Map skillPauses = new EnumMap<>(Skill.class); + private boolean cachedIsLoggedIn = false; + + boolean pauseSkill(Skill skill) + { + return findPauseState(skill).manualPause(); + } + + boolean unpauseSkill(Skill skill) + { + return findPauseState(skill).unpause(); + } + + boolean isPaused(Skill skill) + { + return findPauseState(skill).isPaused(); + } + + void tickXp(Skill skill, int currentXp, int pauseAfterMinutes) + { + final XpPauseStateSingle state = findPauseState(skill); + + if (state.getXp() != currentXp) + { + state.xpChanged(currentXp); + } + else if (pauseAfterMinutes > 0) + { + final long now = System.currentTimeMillis(); + final int pauseAfterMillis = pauseAfterMinutes * 60 * 1000; + final long lastChangeMillis = state.getLastChangeMillis(); + // When config.pauseSkillAfter is 0, it is effectively disabled + if (lastChangeMillis != 0 && (now - lastChangeMillis) >= pauseAfterMillis) + { + state.timeout(); + } + } + } + + void tickLogout(boolean pauseOnLogout, boolean loggedIn) + { + // Deduplicated login and logout calls + if (!cachedIsLoggedIn && loggedIn) + { + cachedIsLoggedIn = true; + + for (Skill skill : Skill.values()) + { + findPauseState(skill).login(); + } + } + else if (cachedIsLoggedIn && !loggedIn) + { + cachedIsLoggedIn = false; + + // If configured, then let the pause state know to pause with reason: logout + if (pauseOnLogout) + { + for (Skill skill : Skill.values()) + { + findPauseState(skill).logout(); + } + } + } + } + + private XpPauseStateSingle findPauseState(Skill skill) + { + return skillPauses.computeIfAbsent(skill, XpPauseStateSingle::new); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java new file mode 100644 index 0000000000..d74d498bd1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java @@ -0,0 +1,99 @@ +/* + * 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.EnumSet; +import java.util.Set; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Skill; + +@RequiredArgsConstructor +class XpPauseStateSingle +{ + @Getter + private final Skill skill; + private final Set pauseReasons = EnumSet.noneOf(XpPauseReason.class); + @Getter + private long lastChangeMillis; + @Getter + private int xp; + + boolean isPaused() + { + return !pauseReasons.isEmpty(); + } + + boolean login() + { + return pauseReasons.remove(XpPauseReason.PAUSED_LOGOUT); + } + + boolean logout() + { + return pauseReasons.add(XpPauseReason.PAUSED_LOGOUT); + } + + boolean timeout() + { + return pauseReasons.add(XpPauseReason.PAUSED_TIMEOUT); + } + + boolean manualPause() + { + return pauseReasons.add(XpPauseReason.PAUSE_MANUAL); + } + + boolean xpChanged(int xp) + { + this.xp = xp; + this.lastChangeMillis = System.currentTimeMillis(); + return clearAll(); + } + + boolean unpause() + { + this.lastChangeMillis = System.currentTimeMillis(); + return clearAll(); + } + + private boolean clearAll() + { + if (pauseReasons.isEmpty()) + { + return false; + } + + pauseReasons.clear(); + return true; + } + + private enum XpPauseReason + { + PAUSE_MANUAL, + PAUSED_LOGOUT, + PAUSED_TIMEOUT + } +} 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 index c70f460a3d..da981077ef 100644 --- 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 @@ -120,6 +120,11 @@ class XpState } } + void tick(Skill skill, long delta) + { + getSkill(skill).tick(delta); + } + /** * 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. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java index 153c53fc3b..82a8849b0c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java @@ -25,8 +25,6 @@ */ package net.runelite.client.plugins.xptracker; -import java.time.Duration; -import java.time.Instant; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -45,7 +43,7 @@ class XpStateSingle @Getter private int xpGained = 0; - private Instant skillTimeStart = null; + private long skillTime = 0; private int actions = 0; private int startLevelExp = 0; private int endLevelExp = 0; @@ -65,7 +63,7 @@ class XpStateSingle private int toHourly(int value) { - if (skillTimeStart == null) + if (skillTime == 0) { return 0; } @@ -75,7 +73,7 @@ class XpStateSingle private long getTimeElapsedInSeconds() { - if (skillTimeStart == null) + if (skillTime == 0) { return 0; } @@ -84,7 +82,7 @@ class XpStateSingle // To prevent that, pretend the skill has been active for a minute (60 seconds) // This will create a lower estimate for the first minute, // but it isn't ridiculous like saying 2 billion XP per hour. - return Math.max(60, Duration.between(skillTimeStart, Instant.now()).getSeconds()); + return Math.max(60, skillTime / 1000); } private int getXpRemaining() @@ -229,15 +227,14 @@ class XpStateSingle endLevelExp = goalEndXp; } - // If this is first time we are updating, we just started tracking - if (skillTimeStart == null) - { - skillTimeStart = Instant.now(); - } - return true; } + public void tick(long delta) + { + skillTime += delta; + } + XpSnapshotSingle snapshot() { return XpSnapshotSingle.builder() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.java new file mode 100644 index 0000000000..f74721c48d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.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 net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("xpTracker") +public interface XpTrackerConfig extends Config +{ + @ConfigItem( + position = 0, + keyName = "logoutPausing", + name = "Pause on Logout", + description = "Configures whether skills should pause on logout" + ) + default boolean pauseOnLogout() + { + return false; + } + + @ConfigItem( + position = 1, + keyName = "pauseSkillAfter", + name = "Auto pause after", + description = "Configures how many minutes passes before pausing a skill while in game and there's no XP, 0 means disabled" + ) + default int pauseSkillAfter() + { + return 0; + } +} 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 1261c19dfd..de27a3edba 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 @@ -28,7 +28,9 @@ package net.runelite.client.plugins.xptracker; import static com.google.common.base.MoreObjects.firstNonNull; import com.google.common.eventbus.Subscribe; import com.google.inject.Binder; +import com.google.inject.Provides; import java.awt.image.BufferedImage; +import java.time.temporal.ChronoUnit; import java.util.EnumSet; import java.util.Objects; import javax.imageio.ImageIO; @@ -43,10 +45,12 @@ import net.runelite.api.WorldType; import net.runelite.api.events.ExperienceChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.client.config.ConfigManager; import net.runelite.client.game.SkillIconManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import static net.runelite.client.plugins.xptracker.XpWorldType.NORMAL; +import net.runelite.client.task.Schedule; import net.runelite.client.ui.NavigationButton; import net.runelite.client.ui.PluginToolbar; import net.runelite.http.api.xp.XpClient; @@ -68,15 +72,24 @@ public class XpTrackerPlugin extends Plugin @Inject private SkillIconManager skillIconManager; + @Inject + private XpTrackerConfig xpTrackerConfig; + private NavigationButton navButton; private XpPanel xpPanel; - - private final XpState xpState = new XpState(); - private XpWorldType lastWorldType; private String lastUsername; + private long lastTickMillis = 0; private final XpClient xpClient = new XpClient(); + private final XpState xpState = new XpState(); + private final XpPauseState xpPauseState = new XpPauseState(); + + @Provides + XpTrackerConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(XpTrackerConfig.class); + } @Override public void configure(Binder binder) @@ -229,7 +242,7 @@ public class XpTrackerPlugin extends Plugin final XpUpdateResult updateResult = xpState.updateSkill(skill, currentXp, startGoalXp, endGoalXp); final boolean updated = XpUpdateResult.UPDATED.equals(updateResult); - xpPanel.updateSkillExperience(updated, skill, xpState.getSkillSnapshot(skill)); + xpPanel.updateSkillExperience(updated, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); xpState.recalculateTotal(); xpPanel.updateTotal(xpState.getTotalSnapshot()); } @@ -237,14 +250,7 @@ public class XpTrackerPlugin extends Plugin @Subscribe public void onGameTick(GameTick event) { - // 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()); + rebuildSkills(); } XpSnapshotSingle getSkillSnapshot(Skill skill) @@ -361,4 +367,59 @@ public class XpTrackerPlugin extends Plugin return null; } } + + @Schedule( + period = 1, + unit = ChronoUnit.SECONDS + ) + public void tickSkillTimes() + { + // Adjust unpause states + for (Skill skill : Skill.values()) + { + xpPauseState.tickXp(skill, client.getSkillExperience(skill), xpTrackerConfig.pauseSkillAfter()); + } + + xpPauseState.tickLogout(xpTrackerConfig.pauseOnLogout(), !GameState.LOGIN_SCREEN.equals(client.getGameState())); + + if (lastTickMillis == 0) + { + lastTickMillis = System.currentTimeMillis(); + return; + } + + final long nowMillis = System.currentTimeMillis(); + final long tickDelta = nowMillis - lastTickMillis; + lastTickMillis = nowMillis; + + for (Skill skill : Skill.values()) + { + if (!xpPauseState.isPaused(skill)) + { + xpState.tick(skill, tickDelta); + } + } + + rebuildSkills(); + } + + private void rebuildSkills() + { + // Rebuild calculated values like xp/hr in panel + for (Skill skill : Skill.values()) + { + xpPanel.updateSkillExperience(false, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + } + + xpState.recalculateTotal(); + xpPanel.updateTotal(xpState.getTotalSnapshot()); + } + + void pauseSkill(Skill skill, boolean pause) + { + if (pause ? xpPauseState.pauseSkill(skill) : xpPauseState.unpauseSkill(skill)) + { + xpPanel.updateSkillExperience(false, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + } + } }