diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java index 512ed1ebd9..6547b26353 100644 --- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java +++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java @@ -95,7 +95,15 @@ public enum VarPlayer SLAYER_GOAL_END(1272), FARMING_GOAL_END(1273), CONSTRUCTION_GOAL_END(1274), - HUNTER_GOAL_END(1275); + HUNTER_GOAL_END(1275), + + /** + * Bird house states + */ + BIRD_HOUSE_MEADOW_NORTH(1626), + BIRD_HOUSE_MEADOW_SOUTH(1627), + BIRD_HOUSE_VALLEY_NORTH(1628), + BIRD_HOUSE_VALLEY_SOUTH(1629); private final int id; } diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java index 3446f76dc6..84c13b5017 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java @@ -241,6 +241,11 @@ public class WidgetID static final int SPEC_ORB = 28; } + static class LoginClickToPlayScreen + { + static final int MESSAGE_OF_THE_DAY = 3; + } + static class Viewport { static final int MINIMAP_RESIZABLE_WIDGET = 17; diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java index df62929276..9beee6509b 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java @@ -139,6 +139,7 @@ public enum WidgetInfo MINIMAP_SPEC_ORB(WidgetID.MINIMAP_GROUP_ID, WidgetID.Minimap.SPEC_ORB), LOGIN_CLICK_TO_PLAY_SCREEN(WidgetID.LOGIN_CLICK_TO_PLAY_GROUP_ID, 0), + LOGIN_CLICK_TO_PLAY_SCREEN_MESSAGE_OF_THE_DAY(WidgetID.LOGIN_CLICK_TO_PLAY_GROUP_ID, WidgetID.LoginClickToPlayScreen.MESSAGE_OF_THE_DAY), FIXED_VIEWPORT(WidgetID.FIXED_VIEWPORT_GROUP_ID, WidgetID.Viewport.FIXED_VIEWPORT), FIXED_VIEWPORT_ROOT_INTERFACE_CONTAINER(WidgetID.FIXED_VIEWPORT_GROUP_ID, WidgetID.FixedViewport.ROOT_INTERFACE_CONTAINER), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerPanel.java deleted file mode 100644 index ab0f4b8d7b..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerPanel.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (c) 2018 Abex - * Copyright (c) 2018, Psikoi - * 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.farmingtracker; - -import com.google.common.base.Strings; -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.format.TextStyle; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import javax.swing.ImageIcon; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.border.EmptyBorder; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.vars.Autoweed; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.game.AsyncBufferedImage; -import net.runelite.client.game.ItemManager; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import net.runelite.client.ui.PluginPanel; -import net.runelite.client.ui.components.materialtabs.MaterialTab; -import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; - -@Slf4j -class FarmingTrackerPanel extends PluginPanel -{ - private final Client client; - private final ItemManager itemManager; - private final ConfigManager configManager; - private final FarmingTrackerConfig config; - - private boolean active; - - private List patchPanels = new ArrayList<>(); - - /* This is the panel the tabs' respective panels will be displayed on. */ - private final JPanel display = new JPanel(); - private final MaterialTabGroup tabGroup = new MaterialTabGroup(display); - - FarmingTrackerPanel( - Client client, - ItemManager itemManager, - ConfigManager configManager, - FarmingTrackerConfig config, - FarmingWorld farmingWorld - ) - { - super(false); - - this.client = client; - this.itemManager = itemManager; - this.configManager = configManager; - this.config = config; - - setLayout(new BorderLayout()); - setBackground(ColorScheme.DARK_GRAY_COLOR); - - display.setBorder(new EmptyBorder(10, 10, 8, 10)); - - tabGroup.setBorder(new EmptyBorder(10, 1, 0, 0)); - - add(tabGroup, BorderLayout.NORTH); - add(display, BorderLayout.CENTER); - - farmingWorld.getTabs().forEach((tab, patches) -> - { - JPanel container = new JPanel(new GridBagLayout()) - { - @Override - public Dimension getPreferredSize() - { - return new Dimension(PluginPanel.PANEL_WIDTH, super.getPreferredSize().height); - } - }; - container.setBackground(ColorScheme.DARK_GRAY_COLOR); - - GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.HORIZONTAL; - c.weightx = 1; - c.gridx = 0; - c.gridy = 0; - - PatchImplementation lastImpl = null; - - boolean first = true; - for (FarmingPatch patch : patches) - { - FarmingPatchPanel p = new FarmingPatchPanel(patch); - - /* Show labels to subdivide tabs into sections */ - if (patch.getImplementation() != lastImpl && !Strings.isNullOrEmpty(patch.getImplementation().getName())) - { - JLabel groupLabel = new JLabel(patch.getImplementation().getName()); - - if (first) - { - first = false; - groupLabel.setBorder(new EmptyBorder(4, 0, 0, 0)); - } - else - { - groupLabel.setBorder(new EmptyBorder(15, 0, 0, 0)); - } - - groupLabel.setFont(FontManager.getRunescapeSmallFont()); - - container.add(groupLabel, c); - c.gridy++; - lastImpl = patch.getImplementation(); - } - - patchPanels.add(p); - container.add(p, c); - c.gridy++; - - /* This is a weird hack to remove the top border on the first tracker of every tab */ - if (first) - { - first = false; - p.setBorder(null); - } - } - - JPanel wrapped = new JPanel(new BorderLayout()); - wrapped.add(container, BorderLayout.NORTH); - wrapped.setBackground(ColorScheme.DARK_GRAY_COLOR); - - JScrollPane scroller = new JScrollPane(wrapped); - scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scroller.getVerticalScrollBar().setPreferredSize(new Dimension(16, 0)); - scroller.getVerticalScrollBar().setBorder(new EmptyBorder(0, 9, 0, 0)); - scroller.setBackground(ColorScheme.DARK_GRAY_COLOR); - - //Use a placeholder icon until the async image gets loaded - MaterialTab materialTab = new MaterialTab(new ImageIcon(), tabGroup, scroller); - materialTab.setPreferredSize(new Dimension(30, 27)); - materialTab.setName(tab.getName()); - - AsyncBufferedImage icon = itemManager.getImage(tab.getItemID()); - Runnable resize = () -> - { - BufferedImage subIcon = icon.getSubimage(0, 0, 32, 32); - materialTab.setIcon(new ImageIcon(subIcon.getScaledInstance(24, 24, Image.SCALE_SMOOTH))); - }; - icon.onChanged(resize); - resize.run(); - - materialTab.setOnSelectEvent(() -> - { - config.setPatch(tab); - return true; - }); - - tabGroup.addTab(materialTab); - if (config.patch() == tab) - { - tabGroup.select(materialTab); - } - }); - } - - void update() - { - if (!active) - { - return; - } - - long unixNow = Instant.now().getEpochSecond(); - log.debug("Updating panel with username {}", client.getUsername()); - boolean autoweed = false; - { - String group = FarmingTrackerConfig.KEY_NAME + "." + client.getUsername(); - autoweed = Integer.toString(Autoweed.ON.ordinal()) - .equals(configManager.getConfiguration(group, FarmingTrackerConfig.AUTOWEED)); - } - for (FarmingPatchPanel panel : patchPanels) - { - FarmingPatch patch = panel.getPatch(); - String group = FarmingTrackerConfig.KEY_NAME + "." + client.getUsername() + "." + patch.getRegion().getRegionID(); - String key = Integer.toString(patch.getVarbit().getId()); - String storedValue = configManager.getConfiguration(group, key); - long unixTime = 0; - int value = 0; - if (storedValue != null) - { - String[] parts = storedValue.split(":"); - if (parts.length == 2) - { - try - { - value = Integer.parseInt(parts[0]); - unixTime = Long.parseLong(parts[1]); - } - catch (NumberFormatException e) - { - } - } - } - - PatchState state = unixTime <= 0 ? null : patch.getImplementation().forVarbitValue(value); - if (state == null) - { - itemManager.getImage(Produce.WEEDS.getItemID()).addTo(panel.getIcon()); - panel.getIcon().setToolTipText("Unknown state"); - panel.getProgress().setMaximumValue(0); - panel.getProgress().setValue(0); - panel.getProgress().setVisible(false); - panel.getEstimate().setText("Unknown"); - panel.getProgress().setBackground(null); - } - else - { - if (state.getProduce().getItemID() < 0) - { - panel.getIcon().setIcon(null); - panel.getIcon().setToolTipText("Unknown state"); - } - else - { - itemManager.getImage(state.getProduce().getItemID()).addTo(panel.getIcon()); - panel.getIcon().setToolTipText(state.getProduce().getName()); - } - - int stage = state.getStage(); - int stages = state.getCropState() == CropState.HARVESTABLE ? - state.getProduce().getHarvestStages() : - state.getProduce().getStages(); - int tickrate = 0; - switch (state.getCropState()) - { - case HARVESTABLE: - tickrate = state.getProduce().getRegrowTickrate() * 60; - break; - case GROWING: - tickrate = state.getProduce().getTickrate() * 60; - break; - } - if (autoweed && state.getProduce() == Produce.WEEDS) - { - stage = 0; - stages = 1; - tickrate = 0; - } - if (tickrate > 0) - { - long tickNow = unixNow / tickrate; - long tickTime = unixTime / tickrate; - int delta = (int) (tickNow - tickTime); - - long doneEstimate = ((stages - 1 - stage) + tickTime) * tickrate; - - stage += delta; - if (stage >= stages) - { - stage = stages - 1; - } - - if (doneEstimate < unixNow) - { - panel.getEstimate().setText("Done"); - } - else if (config.estimateRelative()) - { - int remaining = (int) (59 + doneEstimate - unixNow) / 60; - StringBuilder f = new StringBuilder(); - f.append("Done in "); - int min = remaining % 60; - int hours = (remaining / 60) % 24; - int days = remaining / (60 * 24); - if (days > 0) - { - f.append(days).append("d "); - } - if (hours > 0) - { - f.append(hours).append("h "); - } - if (min > 0) - { - f.append(min).append("m "); - } - panel.getEstimate().setText(f.toString()); - } - else - { - StringBuilder f = new StringBuilder(); - LocalDateTime ldtTime = LocalDateTime.ofEpochSecond(doneEstimate, 0, OffsetDateTime.now().getOffset()); - LocalDateTime ldtNow = LocalDateTime.now(); - f.append("Done "); - if (ldtTime.getDayOfWeek() != ldtNow.getDayOfWeek()) - { - f.append(ldtTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault())).append(" "); - } - f.append(String.format("at %d:%02d", ldtTime.getHour(), ldtTime.getMinute())); - panel.getEstimate().setText(f.toString()); - } - } - else - { - switch (state.getCropState()) - { - case HARVESTABLE: - panel.getEstimate().setText("Done"); - break; - case GROWING: - if (stage == stages - 1) - { - panel.getEstimate().setText("Done"); - } - else - { - panel.getEstimate().setText("Unknown"); - } - break; - case DISEASED: - panel.getEstimate().setText("Diseased"); - break; - case DEAD: - panel.getEstimate().setText("Dead"); - break; - } - } - - /* Hide any fully grown weeds' progress bar. */ - if (state.getProduce() != Produce.WEEDS - || (state.getProduce() == Produce.WEEDS && !autoweed && stage < stages - 1)) - { - panel.getProgress().setVisible(true); - panel.getProgress().setForeground(state.getCropState().getColor().darker()); - panel.getProgress().setMaximumValue(stages - 1); - panel.getProgress().setValue(stage); - panel.getProgress().update(); - } - else - { - panel.getProgress().setVisible(false); - } - } - } - } - - @Override - public void onActivate() - { - active = true; - update(); - } - - @Override - public void onDeactivate() - { - active = false; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerPlugin.java deleted file mode 100644 index 81c4b9c495..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerPlugin.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2018 Abex - * 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.farmingtracker; - -import com.google.common.eventbus.Subscribe; -import com.google.inject.Provides; -import java.awt.image.BufferedImage; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import javax.inject.Inject; -import javax.swing.SwingUtilities; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.Varbits; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.UsernameChanged; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.game.ItemManager; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.task.Schedule; -import net.runelite.client.ui.NavigationButton; -import net.runelite.client.ui.ClientToolbar; -import net.runelite.client.util.ImageUtil; - -@PluginDescriptor( - name = "Farming Tracker", - description = "Show when your farming plots would be fully grown", - tags = {"skilling", "panel", "timers"} -) -@Slf4j -public class FarmingTrackerPlugin extends Plugin -{ - @Inject - private ClientToolbar clientToolbar; - - @Inject - private ConfigManager configManager; - - @Inject - private Client client; - - @Inject - private FarmingWorld farmingWorld; - - @Inject - private ItemManager itemManager; - - @Inject - private FarmingTrackerConfig config; - - private FarmingTrackerPanel panel; - - private NavigationButton navButton; - - private WorldPoint lastTickLoc; - - @Provides - FarmingTrackerConfig provideConfig(ConfigManager configManager) - { - return configManager.getConfig(FarmingTrackerConfig.class); - } - - @Override - protected void startUp() throws Exception - { - final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "farming.png"); - - panel = new FarmingTrackerPanel(client, itemManager, configManager, config, farmingWorld); - - navButton = NavigationButton.builder() - .tooltip("Farming Tracker") - .icon(icon) - .panel(panel) - .priority(4) - .build(); - - clientToolbar.addNavigation(navButton); - - updatePanel(); - } - - @Subscribe - public void onUsernameChanged(UsernameChanged e) - { - updatePanel(); - } - - @Override - protected void shutDown() throws Exception - { - clientToolbar.removeNavigation(navButton); - } - - @Subscribe - public void onGameTick(GameTick t) - { - if (client.getGameState() != GameState.LOGGED_IN) - { - lastTickLoc = null; - return; - } - - WorldPoint loc = lastTickLoc; - lastTickLoc = client.getLocalPlayer().getWorldLocation(); - if (loc == null || loc.getRegionID() != lastTickLoc.getRegionID()) - { - return; - } - - boolean changed = false; - - { - String group = FarmingTrackerConfig.KEY_NAME + "." + client.getUsername(); - String autoweed = Integer.toString(client.getVar(Varbits.AUTOWEED)); - if (!autoweed.equals(configManager.getConfiguration(group, FarmingTrackerConfig.AUTOWEED))) - { - configManager.setConfiguration(group, FarmingTrackerConfig.AUTOWEED, autoweed); - changed = true; - } - } - - FarmingRegion region = farmingWorld.getRegions().get(loc.getRegionID()); - if (region != null && region.isInBounds(loc)) - { - // Write config with new varbits - // farmingTracker...=: - String group = FarmingTrackerConfig.KEY_NAME + "." + client.getUsername() + "." + region.getRegionID(); - long unixNow = Instant.now().getEpochSecond(); - for (Varbits varbit : region.getVarbits()) - { - // Write the config value if it doesn't match what is current, or it is more than 5 minutes old - String key = Integer.toString(varbit.getId()); - String strVarbit = Integer.toString(client.getVar(varbit)); - String storedValue = configManager.getConfiguration(group, key); - if (storedValue != null) - { - String[] parts = storedValue.split(":"); - if (parts.length == 2 && parts[0].equals(strVarbit)) - { - long unixTime = 0; - try - { - unixTime = Long.parseLong(parts[1]); - } - catch (NumberFormatException e) - { - } - if (unixTime + (5 * 60) > unixNow && unixNow + 30 > unixTime) - { - continue; - } - } - } - String value = strVarbit + ":" + unixNow; - configManager.setConfiguration(group, key, value); - changed = true; - } - } - - if (changed) - { - updatePanel(); - } - } - - @Schedule(period = 10, unit = ChronoUnit.SECONDS) - public void updatePanel() - { - SwingUtilities.invokeLater(panel::update); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/OverviewItemPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/OverviewItemPanel.java new file mode 100644 index 0000000000..96cf62c6fc --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/OverviewItemPanel.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.util.ImageUtil; + +class OverviewItemPanel extends JPanel +{ + private static final ImageIcon ARROW_RIGHT_ICON; + + private static final Color HOVER_COLOR = ColorScheme.DARKER_GRAY_HOVER_COLOR; + + private final JLabel statusLabel; + + static + { + ARROW_RIGHT_ICON = new ImageIcon(ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "/util/arrow_right.png")); + } + + OverviewItemPanel(ItemManager itemManager, TimeTrackingPanel pluginPanel, Tab tab, String title) + { + setBackground(ColorScheme.DARKER_GRAY_COLOR); + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(7, 7, 7, 7)); + + JLabel iconLabel = new JLabel(); + iconLabel.setMinimumSize(new Dimension(36, 32)); + itemManager.getImage(tab.getItemID()).addTo(iconLabel); + add(iconLabel, BorderLayout.WEST); + + JPanel textContainer = new JPanel(); + textContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + textContainer.setLayout(new GridLayout(2, 1)); + textContainer.setBorder(new EmptyBorder(5, 7, 5, 7)); + + addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + pluginPanel.switchTab(tab); + setBackground(ColorScheme.DARKER_GRAY_COLOR); + textContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + } + + @Override + public void mouseReleased(MouseEvent e) + { + setBackground(HOVER_COLOR); + textContainer.setBackground(HOVER_COLOR); + } + + @Override + public void mouseEntered(MouseEvent e) + { + setBackground(HOVER_COLOR); + textContainer.setBackground(HOVER_COLOR); + setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + + @Override + public void mouseExited(MouseEvent e) + { + setBackground(ColorScheme.DARKER_GRAY_COLOR); + textContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + }); + + JLabel titleLabel = new JLabel(title); + titleLabel.setForeground(Color.WHITE); + titleLabel.setFont(FontManager.getRunescapeSmallFont()); + + statusLabel = new JLabel(); + statusLabel.setForeground(Color.GRAY); + statusLabel.setFont(FontManager.getRunescapeSmallFont()); + + textContainer.add(titleLabel); + textContainer.add(statusLabel); + + add(textContainer, BorderLayout.CENTER); + + JLabel arrowLabel = new JLabel(ARROW_RIGHT_ICON); + add(arrowLabel, BorderLayout.EAST); + } + + void updateStatus(String text, Color color) + { + statusLabel.setText(text); + statusLabel.setForeground(color); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/OverviewTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/OverviewTabPanel.java new file mode 100644 index 0000000000..38bd105709 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/OverviewTabPanel.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking; + +import java.awt.Color; +import java.awt.GridLayout; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.timetracking.clocks.ClockManager; +import net.runelite.client.plugins.timetracking.farming.FarmingTracker; +import net.runelite.client.plugins.timetracking.farming.PatchImplementation; +import net.runelite.client.plugins.timetracking.hunter.BirdHouseTracker; +import net.runelite.client.ui.ColorScheme; + +class OverviewTabPanel extends TabContentPanel +{ + private final TimeTrackingConfig config; + private final FarmingTracker farmingTracker; + private final BirdHouseTracker birdHouseTracker; + private final ClockManager clockManager; + + private final OverviewItemPanel timerOverview; + private final OverviewItemPanel stopwatchOverview; + private final Map farmingOverviews; + private final OverviewItemPanel birdHouseOverview; + + OverviewTabPanel(ItemManager itemManager, TimeTrackingConfig config, TimeTrackingPanel pluginPanel, + FarmingTracker farmingTracker, BirdHouseTracker birdHouseTracker, ClockManager clockManager) + { + this.config = config; + this.farmingTracker = farmingTracker; + this.birdHouseTracker = birdHouseTracker; + this.clockManager = clockManager; + + setLayout(new GridLayout(0, 1, 0, 8)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + timerOverview = new OverviewItemPanel(itemManager, pluginPanel, Tab.CLOCK, "Timers"); + add(timerOverview); + + stopwatchOverview = new OverviewItemPanel(itemManager, pluginPanel, Tab.CLOCK, "Stopwatches"); + add(stopwatchOverview); + + birdHouseOverview = new OverviewItemPanel(itemManager, pluginPanel, Tab.BIRD_HOUSE, "Bird Houses"); + add(birdHouseOverview); + + farmingOverviews = new LinkedHashMap<>(); + farmingOverviews.put(PatchImplementation.ALLOTMENT, new OverviewItemPanel(itemManager, pluginPanel, Tab.ALLOTMENT, "Allotment Patches")); + farmingOverviews.put(PatchImplementation.FLOWER, new OverviewItemPanel(itemManager, pluginPanel, Tab.FLOWER, "Flower Patches")); + farmingOverviews.put(PatchImplementation.HERB, new OverviewItemPanel(itemManager, pluginPanel, Tab.HERB, "Herb Patches")); + farmingOverviews.put(PatchImplementation.TREE, new OverviewItemPanel(itemManager, pluginPanel, Tab.TREE, "Tree Patches")); + farmingOverviews.put(PatchImplementation.FRUIT_TREE, new OverviewItemPanel(itemManager, pluginPanel, Tab.FRUIT_TREE, "Fruit Tree Patches")); + farmingOverviews.put(PatchImplementation.HOPS, new OverviewItemPanel(itemManager, pluginPanel, Tab.HOPS, "Hops Patches")); + farmingOverviews.put(PatchImplementation.BUSH, new OverviewItemPanel(itemManager, pluginPanel, Tab.BUSH, "Bush Patches")); + farmingOverviews.put(PatchImplementation.GRAPES, new OverviewItemPanel(itemManager, pluginPanel, Tab.GRAPE, "Grape Patches")); + + for (OverviewItemPanel panel : farmingOverviews.values()) + { + add(panel); + } + } + + @Override + public int getUpdateInterval() + { + return 50; // 10 seconds + } + + @Override + public void update() + { + final long timers = clockManager.getActiveTimerCount(); + final long stopwatches = clockManager.getActiveStopwatchCount(); + + if (timers == 0) + { + timerOverview.updateStatus("No active timers", Color.GRAY); + } + else + { + timerOverview.updateStatus(timers + " active timer" + (timers == 1 ? "" : "s"), ColorScheme.PROGRESS_COMPLETE_COLOR); + } + + if (stopwatches == 0) + { + stopwatchOverview.updateStatus("No active stopwatches", Color.GRAY); + } + else + { + stopwatchOverview.updateStatus(stopwatches + " active stopwatch" + (stopwatches == 1 ? "" : "es"), ColorScheme.PROGRESS_COMPLETE_COLOR); + } + + farmingOverviews.forEach((patchType, panel) -> updateItemPanel(panel, farmingTracker.getCompletionTime(patchType))); + updateItemPanel(birdHouseOverview, birdHouseTracker.getCompletionTime()); + } + + private void updateItemPanel(OverviewItemPanel panel, long completionTime) + { + long duration = completionTime - Instant.now().getEpochSecond(); + + if (completionTime < 0) + { + panel.updateStatus("Unknown", Color.GRAY); + } + else if (duration <= 0) + { + panel.updateStatus("Ready", ColorScheme.PROGRESS_COMPLETE_COLOR); + } + else + { + panel.updateStatus("Ready " + getFormattedEstimate(duration, config.estimateRelative()), Color.GRAY); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/Tab.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/Tab.java similarity index 68% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/Tab.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/Tab.java index 50f50b2ed8..4dfa94c8b9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/Tab.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/Tab.java @@ -22,7 +22,7 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -32,12 +32,20 @@ import net.runelite.api.ItemID; @Getter public enum Tab { - ALLOTMENT("Allotments", ItemID.CABBAGE), - HERB("Herbs", ItemID.GRIMY_RANARR_WEED), - TREE("Trees", ItemID.MAHOGANY_LOGS), - FRUIT_TREE("Fruit Trees", ItemID.PINEAPPLE), - BUSH("Bushes", ItemID.REDBERRIES), - SPECIAL("Special", ItemID.MUSHROOM); + OVERVIEW("Overview", ItemID.OLD_NOTES), + CLOCK("Timers & Stopwatches", ItemID.WATCH), + BIRD_HOUSE("Bird Houses", ItemID.OAK_BIRD_HOUSE), + ALLOTMENT("Allotment Patches", ItemID.CABBAGE), + FLOWER("Flower Patches", ItemID.RED_FLOWERS), + HERB("Herb Patches", ItemID.GRIMY_RANARR_WEED), + TREE("Tree Patches", ItemID.YEW_LOGS), + FRUIT_TREE("Fruit Tree Patches", ItemID.PINEAPPLE), + HOPS("Hops Patches", ItemID.BARLEY), + BUSH("Bush Patches", ItemID.POISON_IVY_BERRIES), + GRAPE("Grape Patches", ItemID.GRAPES), + SPECIAL("Special Patches", ItemID.MUSHROOM); + + public static final Tab[] FARMING_TABS = {ALLOTMENT, FLOWER, HERB, TREE, FRUIT_TREE, HOPS, BUSH, GRAPE, SPECIAL}; private final String name; private final int itemID; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TabContentPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TabContentPanel.java new file mode 100644 index 0000000000..47740a8b17 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TabContentPanel.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking; + +import java.awt.Dimension; +import java.time.LocalDateTime; +import java.time.format.TextStyle; +import java.time.temporal.ChronoUnit; +import java.util.Locale; +import javax.swing.JPanel; + +public abstract class TabContentPanel extends JPanel +{ + /** + * Gets the update interval of this panel, in units of 200 milliseconds + * (the plugin panel checks if its contents should be updated every 200 ms; + * this can be considered its "tick rate"). + */ + public abstract int getUpdateInterval(); + + public abstract void update(); + + @Override + public Dimension getPreferredSize() + { + return super.getPreferredSize(); + } + + protected static String getFormattedEstimate(long remainingSeconds, boolean useRelativeTime) + { + if (useRelativeTime) + { + StringBuilder sb = new StringBuilder("in "); + long duration = (remainingSeconds + 59) / 60; + long minutes = duration % 60; + long hours = (duration / 60) % 24; + long days = duration / (60 * 24); + if (days > 0) + { + sb.append(days).append("d "); + } + if (hours > 0) + { + sb.append(hours).append("h "); + } + if (minutes > 0) + { + sb.append(minutes).append("m "); + } + return sb.toString(); + } + else + { + StringBuilder sb = new StringBuilder(); + LocalDateTime endTime = LocalDateTime.now().plus(remainingSeconds, ChronoUnit.SECONDS); + LocalDateTime currentTime = LocalDateTime.now(); + if (endTime.getDayOfWeek() != currentTime.getDayOfWeek()) + { + sb.append(endTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault())).append(" "); + } + sb.append(String.format("at %d:%02d", endTime.getHour(), endTime.getMinute())); + return sb.toString(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java similarity index 63% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerConfig.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java index 30653b5aa9..744250a8db 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingTrackerConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java @@ -22,22 +22,26 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; -@ConfigGroup("farmingTracker") -public interface FarmingTrackerConfig extends Config +@ConfigGroup("timetracking") +public interface TimeTrackingConfig extends Config { - String KEY_NAME = "farmingTracker"; + String CONFIG_GROUP = "timetracking"; String AUTOWEED = "autoweed"; + String BIRD_HOUSE = "birdhouse"; + String TIMERS = "timers"; + String STOPWATCHES = "stopwatches"; @ConfigItem( keyName = "estimateRelative", name = "Show relative time", - description = "Show amount of time remaining for a patch, opposed to when the patch is finished" + description = "Show amount of time remaining instead of completion time", + position = 1 ) default boolean estimateRelative() { @@ -45,21 +49,43 @@ public interface FarmingTrackerConfig extends Config } @ConfigItem( - keyName = "patch", - name = "Default patch", - description = "Default patch on opening the panel", - hidden = true + keyName = "timerNotification", + name = "Timer notification", + description = "Notify you whenever a timer has finished counting down", + position = 2 ) - default Tab patch() + default boolean timerNotification() { - return Tab.ALLOTMENT; + return false; } @ConfigItem( - keyName = "patch", + keyName = "birdHouseNotification", + name = "Bird house notification", + description = "Notify you when all bird houses are full", + position = 3 + ) + default boolean birdHouseNotification() + { + return false; + } + + @ConfigItem( + keyName = "activeTab", + name = "Active Tab", + description = "The currently selected tab", + hidden = true + ) + default Tab activeTab() + { + return Tab.CLOCK; + } + + @ConfigItem( + keyName = "activeTab", name = "", description = "", hidden = true ) - void setPatch(Tab t); -} \ No newline at end of file + void setActiveTab(Tab t); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPanel.java new file mode 100644 index 0000000000..81a19cb483 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPanel.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2018 Abex + * Copyright (c) 2018, Psikoi + * 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.timetracking; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.game.AsyncBufferedImage; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.timetracking.clocks.ClockManager; +import net.runelite.client.plugins.timetracking.farming.FarmingTracker; +import net.runelite.client.plugins.timetracking.hunter.BirdHouseTracker; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.materialtabs.MaterialTab; +import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; + +@Slf4j +class TimeTrackingPanel extends PluginPanel +{ + private final ItemManager itemManager; + private final TimeTrackingConfig config; + + /* This is the panel the tabs' respective panels will be displayed on. */ + private final JPanel display = new JPanel(); + private final Map uiTabs = new HashMap<>(); + private final MaterialTabGroup tabGroup = new MaterialTabGroup(display); + + private boolean active; + + @Nullable + private TabContentPanel activeTabPanel = null; + + TimeTrackingPanel(ItemManager itemManager, TimeTrackingConfig config, + FarmingTracker farmingTracker, BirdHouseTracker birdHouseTracker, ClockManager clockManager) + { + super(false); + + this.itemManager = itemManager; + this.config = config; + + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + display.setBorder(new EmptyBorder(10, 10, 8, 10)); + + tabGroup.setLayout(new GridLayout(0, 6, 7, 7)); + tabGroup.setBorder(new EmptyBorder(10, 10, 0, 10)); + + add(tabGroup, BorderLayout.NORTH); + add(display, BorderLayout.CENTER); + + addTab(Tab.OVERVIEW, new OverviewTabPanel(itemManager, config, this, farmingTracker, birdHouseTracker, clockManager)); + addTab(Tab.CLOCK, clockManager.getClockTabPanel()); + addTab(Tab.BIRD_HOUSE, birdHouseTracker.createBirdHouseTabPanel()); + + for (Tab tab : Tab.FARMING_TABS) + { + addTab(tab, farmingTracker.createTabPanel(tab)); + } + } + + private void addTab(Tab tab, TabContentPanel tabContentPanel) + { + JPanel wrapped = new JPanel(new BorderLayout()); + wrapped.add(tabContentPanel, BorderLayout.NORTH); + wrapped.setBackground(ColorScheme.DARK_GRAY_COLOR); + + JScrollPane scroller = new JScrollPane(wrapped); + scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scroller.getVerticalScrollBar().setPreferredSize(new Dimension(16, 0)); + scroller.getVerticalScrollBar().setBorder(new EmptyBorder(0, 9, 0, 0)); + scroller.setBackground(ColorScheme.DARK_GRAY_COLOR); + + // Use a placeholder icon until the async image gets loaded + MaterialTab materialTab = new MaterialTab(new ImageIcon(), tabGroup, scroller); + materialTab.setPreferredSize(new Dimension(30, 27)); + materialTab.setName(tab.getName()); + materialTab.setToolTipText(tab.getName()); + + AsyncBufferedImage icon = itemManager.getImage(tab.getItemID()); + Runnable resize = () -> + { + BufferedImage subIcon = icon.getSubimage(0, 0, 32, 32); + materialTab.setIcon(new ImageIcon(subIcon.getScaledInstance(24, 24, Image.SCALE_SMOOTH))); + }; + icon.onChanged(resize); + resize.run(); + + materialTab.setOnSelectEvent(() -> + { + config.setActiveTab(tab); + activeTabPanel = tabContentPanel; + + tabContentPanel.update(); + return true; + }); + + uiTabs.put(tab, materialTab); + tabGroup.addTab(materialTab); + + if (config.activeTab() == tab) + { + tabGroup.select(materialTab); + } + } + + void switchTab(Tab tab) + { + tabGroup.select(uiTabs.get(tab)); + } + + /** + * Gets the update interval of the active tab panel, in units of 200 milliseconds. + */ + int getUpdateInterval() + { + return activeTabPanel == null ? Integer.MAX_VALUE : activeTabPanel.getUpdateInterval(); + } + + /** + * Updates the active tab panel, if this plugin panel is displayed. + */ + void update() + { + if (!active || activeTabPanel == null) + { + return; + } + + SwingUtilities.invokeLater(activeTabPanel::update); + } + + @Override + public void onActivate() + { + active = true; + update(); + } + + @Override + public void onDeactivate() + { + active = false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java new file mode 100644 index 0000000000..d8b4da011f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2018 Abex + * Copyright (c) 2018, Daniel Teo + * 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.timetracking; + +import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; +import java.awt.image.BufferedImage; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.UsernameChanged; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.CONFIG_GROUP; +import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.STOPWATCHES; +import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.TIMERS; +import net.runelite.client.plugins.timetracking.clocks.ClockManager; +import net.runelite.client.plugins.timetracking.farming.FarmingTracker; +import net.runelite.client.plugins.timetracking.hunter.BirdHouseTracker; +import net.runelite.client.task.Schedule; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.util.ImageUtil; + +@PluginDescriptor( + name = "Time Tracking", + description = "Enable the Time Tracking panel, which contains timers, stopwatches, and farming and bird house trackers", + tags = {"birdhouse", "farming", "hunter", "notifications", "skilling", "stopwatches", "timers", "panel"} +) +@Slf4j +public class TimeTrackingPlugin extends Plugin +{ + @Inject + private ClientToolbar clientToolbar; + + @Inject + private Client client; + + @Inject + private FarmingTracker farmingTracker; + + @Inject + private BirdHouseTracker birdHouseTracker; + + @Inject + private ClockManager clockManager; + + @Inject + private ItemManager itemManager; + + @Inject + private TimeTrackingConfig config; + + @Inject + private ScheduledExecutorService executorService; + + private ScheduledFuture panelUpdateFuture; + + private TimeTrackingPanel panel; + + private NavigationButton navButton; + + private WorldPoint lastTickLocation; + private boolean lastTickPostLogin; + + @Provides + TimeTrackingConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(TimeTrackingConfig.class); + } + + @Override + protected void startUp() throws Exception + { + clockManager.loadTimers(); + clockManager.loadStopwatches(); + birdHouseTracker.loadFromConfig(); + farmingTracker.loadCompletionTimes(); + + final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "watch.png"); + + panel = new TimeTrackingPanel(itemManager, config, farmingTracker, birdHouseTracker, clockManager); + + navButton = NavigationButton.builder() + .tooltip("Time Tracking") + .icon(icon) + .panel(panel) + .priority(4) + .build(); + + clientToolbar.addNavigation(navButton); + + panelUpdateFuture = executorService.scheduleAtFixedRate(this::updatePanel, 200, 200, TimeUnit.MILLISECONDS); + } + + @Override + protected void shutDown() throws Exception + { + lastTickLocation = null; + lastTickPostLogin = false; + + if (panelUpdateFuture != null) + { + panelUpdateFuture.cancel(true); + panelUpdateFuture = null; + } + + clientToolbar.removeNavigation(navButton); + } + + @Subscribe + public void onConfigChanged(ConfigChanged e) + { + if (!e.getGroup().equals(CONFIG_GROUP)) + { + return; + } + + if (clockManager.getTimers().isEmpty() && e.getKey().equals(TIMERS)) + { + clockManager.loadTimers(); + } + else if (clockManager.getStopwatches().isEmpty() && e.getKey().equals(STOPWATCHES)) + { + clockManager.loadStopwatches(); + } + } + + @Subscribe + public void onGameTick(GameTick t) + { + if (client.getGameState() != GameState.LOGGED_IN) + { + lastTickLocation = null; + return; + } + + // bird house data is only sent after exiting the post-login screen + Widget motd = client.getWidget(WidgetInfo.LOGIN_CLICK_TO_PLAY_SCREEN_MESSAGE_OF_THE_DAY); + if (motd != null && !motd.isHidden()) + { + lastTickPostLogin = true; + return; + } + + if (lastTickPostLogin) + { + lastTickPostLogin = false; + return; + } + + WorldPoint loc = lastTickLocation; + lastTickLocation = client.getLocalPlayer().getWorldLocation(); + + if (loc == null || loc.getPlane() != 0 || loc.getRegionID() != lastTickLocation.getRegionID()) + { + return; + } + + boolean birdHouseDataChanged = birdHouseTracker.updateData(loc); + boolean farmingDataChanged = farmingTracker.updateData(loc); + + if (birdHouseDataChanged || farmingDataChanged) + { + panel.update(); + } + } + + @Subscribe + public void onUsernameChanged(UsernameChanged e) + { + farmingTracker.migrateConfiguration(); + farmingTracker.loadCompletionTimes(); + birdHouseTracker.loadFromConfig(); + panel.update(); + } + + @Schedule(period = 10, unit = ChronoUnit.SECONDS) + public void checkCompletion() + { + boolean birdHouseDataChanged = birdHouseTracker.checkCompletion(); + + if (birdHouseDataChanged) + { + panel.update(); + } + } + + private void updatePanel() + { + long unitTime = Instant.now().toEpochMilli() / 200; + + boolean clockDataChanged = false; + + if (unitTime % 5 == 0) + { + clockDataChanged = clockManager.checkCompletion(); + } + + if (unitTime % panel.getUpdateInterval() == 0 || clockDataChanged) + { + panel.update(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingPatchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java similarity index 56% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingPatchPanel.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java index aa3162b1ad..8bbb5ae4ab 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingPatchPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java @@ -7,25 +7,24 @@ * 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. + * 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. + * 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.farmingtracker; +package net.runelite.client.plugins.timetracking; -import com.google.common.base.Strings; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; @@ -40,16 +39,16 @@ import net.runelite.client.ui.components.ThinProgressBar; import net.runelite.client.ui.components.shadowlabel.JShadowedLabel; @Getter -class FarmingPatchPanel extends JPanel +public class TimeablePanel extends JPanel { - private final FarmingPatch patch; + private final T timeable; private final JLabel icon = new JLabel(); private final JLabel estimate = new JLabel(); private final ThinProgressBar progress = new ThinProgressBar(); - FarmingPatchPanel(FarmingPatch patch) + public TimeablePanel(T timeable, String title, int maximumProgressValue) { - this.patch = patch; + this.timeable = timeable; setLayout(new BorderLayout()); setBorder(new EmptyBorder(7, 0, 0, 0)); @@ -66,8 +65,7 @@ class FarmingPatchPanel extends JPanel infoPanel.setLayout(new GridLayout(2, 1)); infoPanel.setBorder(new EmptyBorder(4, 4, 4, 0)); - final JLabel location = new JShadowedLabel(patch.getRegion().getName() - + (Strings.isNullOrEmpty(patch.getName()) ? "" : " (" + patch.getName() + ")")); + final JLabel location = new JShadowedLabel(title); location.setFont(FontManager.getRunescapeSmallFont()); location.setForeground(Color.WHITE); @@ -80,6 +78,9 @@ class FarmingPatchPanel extends JPanel topContainer.add(icon, BorderLayout.WEST); topContainer.add(infoPanel, BorderLayout.CENTER); + progress.setValue(0); + progress.setMaximumValue(maximumProgressValue); + add(topContainer, BorderLayout.NORTH); add(progress, BorderLayout.SOUTH); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Clock.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Clock.java new file mode 100644 index 0000000000..31300069c8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Clock.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +abstract class Clock +{ + protected String name; + + // last updated time (recorded as seconds since epoch) + protected long lastUpdate; + + // whether the clock is currently running + protected boolean active; + + Clock(String name) + { + this.name = name; + this.lastUpdate = Instant.now().getEpochSecond(); + this.active = false; + } + + abstract long getDisplayTime(); + + abstract void setDuration(long duration); + + abstract boolean start(); + + abstract boolean pause(); + + abstract void reset(); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockManager.java new file mode 100644 index 0000000000..374bc43ae6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockManager.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.inject.Singleton; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import javax.inject.Inject; +import javax.swing.SwingUtilities; +import joptsimple.internal.Strings; +import lombok.Getter; +import net.runelite.client.Notifier; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.timetracking.TimeTrackingConfig; + +@Singleton +public class ClockManager +{ + private static final long DEFAULT_TIMER_DURATION = 60 * 5; // 5 minutes + + @Inject + private ConfigManager configManager; + + @Inject + private TimeTrackingConfig config; + + @Inject + private Notifier notifier; + + @Getter + private final List timers = new CopyOnWriteArrayList<>(); + + @Getter + private final List stopwatches = new ArrayList<>(); + + @Getter + private ClockTabPanel clockTabPanel = new ClockTabPanel(this); + + void addTimer() + { + timers.add(new Timer("Timer " + (timers.size() + 1), DEFAULT_TIMER_DURATION)); + saveTimers(); + + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + + void addStopwatch() + { + stopwatches.add(new Stopwatch("Stopwatch " + (stopwatches.size() + 1))); + saveStopwatches(); + + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + + void removeTimer(Timer timer) + { + timers.remove(timer); + saveTimers(); + + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + + void removeStopwatch(Stopwatch stopwatch) + { + stopwatches.remove(stopwatch); + saveStopwatches(); + + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + + public long getActiveTimerCount() + { + return timers.stream().filter(Timer::isActive).count(); + } + + public long getActiveStopwatchCount() + { + return stopwatches.stream().filter(Stopwatch::isActive).count(); + } + + /** + * Checks if any timers have completed, and send notifications if required. + */ + public boolean checkCompletion() + { + boolean changed = false; + + for (Timer timer : timers) + { + if (timer.isActive() && timer.getDisplayTime() == 0) + { + timer.pause(); + changed = true; + + if (config.timerNotification()) + { + notifier.notify("[" + timer.getName() + "] has finished counting down."); + } + } + } + + if (changed) + { + saveTimers(); + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + + return changed; + } + + public void loadTimers() + { + final String timersJson = configManager.getConfiguration(TimeTrackingConfig.CONFIG_GROUP, TimeTrackingConfig.TIMERS); + + if (!Strings.isNullOrEmpty(timersJson)) + { + final Gson gson = new Gson(); + final List timers = gson.fromJson(timersJson, new TypeToken>() + { + }.getType()); + + this.timers.clear(); + this.timers.addAll(timers); + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + } + + public void loadStopwatches() + { + final String stopwatchesJson = configManager.getConfiguration(TimeTrackingConfig.CONFIG_GROUP, TimeTrackingConfig.STOPWATCHES); + + if (!Strings.isNullOrEmpty(stopwatchesJson)) + { + final Gson gson = new Gson(); + final List stopwatches = gson.fromJson(stopwatchesJson, new TypeToken>() + { + }.getType()); + + this.stopwatches.clear(); + this.stopwatches.addAll(stopwatches); + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + } + + public void clear() + { + timers.clear(); + stopwatches.clear(); + + SwingUtilities.invokeLater(clockTabPanel::rebuild); + } + + void saveToConfig() + { + saveTimers(); + saveStopwatches(); + } + + void saveTimers() + { + final Gson gson = new Gson(); + final String json = gson.toJson(timers); + configManager.setConfiguration(TimeTrackingConfig.CONFIG_GROUP, TimeTrackingConfig.TIMERS, json); + } + + void saveStopwatches() + { + final Gson gson = new Gson(); + final String json = gson.toJson(stopwatches); + configManager.setConfiguration(TimeTrackingConfig.CONFIG_GROUP, TimeTrackingConfig.STOPWATCHES, json); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java new file mode 100644 index 0000000000..3b46a6c22b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import lombok.Getter; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.components.FlatTextField; +import net.runelite.client.ui.components.IconButton; + +abstract class ClockPanel extends JPanel +{ + private static final Border NAME_BOTTOM_BORDER = new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ColorScheme.DARK_GRAY_COLOR), + BorderFactory.createLineBorder(ColorScheme.DARKER_GRAY_COLOR)); + + private static final Color ACTIVE_CLOCK_COLOR = ColorScheme.LIGHT_GRAY_COLOR.brighter(); + private static final Color INACTIVE_CLOCK_COLOR = ColorScheme.LIGHT_GRAY_COLOR.darker(); + + // additional content or buttons should be added to these panels in the subclasses + final JPanel contentContainer; + final JPanel leftActions; + final JPanel rightActions; + + private final FlatTextField nameInput; + private final IconButton startPauseButton; + private final FlatTextField displayInput; + + @Getter + private final Clock clock; + + private final String clockType; + private final boolean editable; + + ClockPanel(ClockManager clockManager, Clock clock, String clockType, boolean editable) + { + this.clock = clock; + this.clockType = clockType; + this.editable = editable; + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(3, 0, 0, 0)); + + JPanel nameWrapper = new JPanel(new BorderLayout()); + nameWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR); + nameWrapper.setBorder(NAME_BOTTOM_BORDER); + + nameInput = new FlatTextField(); + nameInput.setText(clock.getName()); + nameInput.setBorder(null); + nameInput.setBackground(ColorScheme.DARKER_GRAY_COLOR); + nameInput.setPreferredSize(new Dimension(0, 24)); + nameInput.getTextField().setBorder(new EmptyBorder(0, 8, 0, 0)); + nameInput.addActionListener(e -> getParent().requestFocusInWindow()); + + nameInput.getTextField().addFocusListener(new FocusListener() + { + @Override + public void focusGained(FocusEvent e) + { + nameInput.getTextField().selectAll(); + } + + @Override + public void focusLost(FocusEvent e) + { + clock.setName(nameInput.getText()); + clockManager.saveToConfig(); + } + }); + + nameWrapper.add(nameInput, BorderLayout.CENTER); + + JPanel mainContainer = new JPanel(new BorderLayout()); + mainContainer.setBorder(new EmptyBorder(5, 0, 0, 0)); + mainContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + contentContainer = new JPanel(new BorderLayout()); + contentContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + displayInput = new FlatTextField(); + displayInput.setEditable(editable); + displayInput.setBorder(null); + displayInput.setBackground(ColorScheme.DARKER_GRAY_COLOR); + displayInput.setPreferredSize(new Dimension(0, 24)); + displayInput.getTextField().setHorizontalAlignment(SwingConstants.CENTER); + displayInput.addActionListener(e -> getParent().requestFocusInWindow()); + + displayInput.getTextField().addFocusListener(new FocusListener() + { + @Override + public void focusGained(FocusEvent e) + { + displayInput.getTextField().setForeground(INACTIVE_CLOCK_COLOR); + displayInput.getTextField().selectAll(); + } + + @Override + public void focusLost(FocusEvent e) + { + String[] parts = displayInput.getText().split(":"); + long duration = 0; + + // parse from back to front, so as to accept hour:min:sec, min:sec, and sec formats + for (int i = parts.length - 1, multiplier = 1; i >= 0 && multiplier <= 3600; i--, multiplier *= 60) + { + try + { + duration += Integer.parseInt(parts[i].trim()) * multiplier; + } + catch (NumberFormatException nfe) + { + // ignored + } + } + + clock.setDuration(Math.max(0, duration)); + clock.reset(); + updateDisplayInput(); + updateActivityStatus(); + clockManager.saveTimers(); + } + }); + + updateDisplayInput(); + + contentContainer.add(displayInput, BorderLayout.NORTH); + + JPanel actionsBar = new JPanel(new BorderLayout()); + actionsBar.setBorder(new EmptyBorder(4, 0, 4, 0)); + actionsBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + leftActions = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + leftActions.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + startPauseButton = new IconButton(ClockTabPanel.START_ICON); + startPauseButton.setPreferredSize(new Dimension(16, 14)); + updateActivityStatus(); + + startPauseButton.addMouseListener(new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent e) + { + startPauseButton.setIcon(clock.isActive() ? ClockTabPanel.PAUSE_ICON_HOVER : ClockTabPanel.START_ICON_HOVER); + } + + @Override + public void mouseExited(MouseEvent e) + { + startPauseButton.setIcon(clock.isActive() ? ClockTabPanel.PAUSE_ICON : ClockTabPanel.START_ICON); + } + }); + + startPauseButton.addActionListener(e -> + { + if (clock.isActive()) + { + clock.pause(); + } + else if (!clock.start()) + { + return; + } + + updateActivityStatus(); + clockManager.saveToConfig(); + }); + + IconButton resetButton = new IconButton(ClockTabPanel.RESET_ICON, ClockTabPanel.RESET_ICON_HOVER); + resetButton.setPreferredSize(new Dimension(16, 14)); + resetButton.setToolTipText("Reset " + clockType); + + resetButton.addActionListener(e -> + { + clock.reset(); + reset(); + clockManager.saveToConfig(); + }); + + leftActions.add(startPauseButton); + leftActions.add(resetButton); + + rightActions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); + rightActions.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + actionsBar.add(leftActions, BorderLayout.WEST); + actionsBar.add(rightActions, BorderLayout.EAST); + + mainContainer.add(contentContainer, BorderLayout.CENTER); + mainContainer.add(actionsBar, BorderLayout.SOUTH); + + add(nameWrapper, BorderLayout.NORTH); + add(mainContainer, BorderLayout.CENTER); + } + + void reset() + { + updateDisplayInput(); + updateActivityStatus(); + } + + void updateDisplayInput() + { + if (!displayInput.getTextField().hasFocus()) + { + displayInput.setText(getFormattedDuration(clock.getDisplayTime())); + } + } + + void updateActivityStatus() + { + boolean isActive = clock.isActive(); + + displayInput.setEditable(editable && !isActive); + displayInput.getTextField().setForeground(isActive ? ACTIVE_CLOCK_COLOR : INACTIVE_CLOCK_COLOR); + startPauseButton.setToolTipText(isActive ? "Pause " + clockType : "Start " + clockType); + startPauseButton.setIcon(isActive ? ClockTabPanel.PAUSE_ICON : ClockTabPanel.START_ICON); + + if (editable && clock.getDisplayTime() == 0 && !isActive) + { + displayInput.getTextField().setForeground(ColorScheme.PROGRESS_ERROR_COLOR.darker()); + } + } + + static String getFormattedDuration(long duration) + { + long hours = duration / (60 * 60); + long mins = (duration / 60) % 60; + long seconds = duration % 60; + + return String.format("%02d:%02d:%02d", hours, mins, seconds); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java new file mode 100644 index 0000000000..9837155eef --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import net.runelite.client.plugins.timetracking.TabContentPanel; +import net.runelite.client.plugins.timetracking.TimeTrackingPlugin; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.IconButton; +import net.runelite.client.ui.components.shadowlabel.JShadowedLabel; +import net.runelite.client.util.ImageUtil; + +public class ClockTabPanel extends TabContentPanel +{ + static final ImageIcon DELETE_ICON; + static final ImageIcon DELETE_ICON_HOVER; + static final ImageIcon LAP_ICON; + static final ImageIcon LAP_ICON_HOVER; + static final ImageIcon PAUSE_ICON; + static final ImageIcon PAUSE_ICON_HOVER; + static final ImageIcon RESET_ICON; + static final ImageIcon RESET_ICON_HOVER; + static final ImageIcon START_ICON; + static final ImageIcon START_ICON_HOVER; + + private static final ImageIcon ADD_ICON; + private static final ImageIcon ADD_ICON_HOVER; + + private final ClockManager clockManager; + + private final List clockPanels = new ArrayList<>(); + + static + { + BufferedImage deleteIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "delete_icon.png"); + BufferedImage lapIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "lap_icon.png"); + BufferedImage pauseIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "pause_icon.png"); + BufferedImage resetIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "reset_icon.png"); + BufferedImage startIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "start_icon.png"); + BufferedImage addIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "add_icon.png"); + + DELETE_ICON = new ImageIcon(deleteIcon); + DELETE_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(deleteIcon, -80)); + LAP_ICON = new ImageIcon(lapIcon); + LAP_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(lapIcon, -80)); + PAUSE_ICON = new ImageIcon(pauseIcon); + PAUSE_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(pauseIcon, -80)); + RESET_ICON = new ImageIcon(resetIcon); + RESET_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(resetIcon, -80)); + START_ICON = new ImageIcon(startIcon); + START_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(startIcon, -80)); + ADD_ICON = new ImageIcon(addIcon); + ADD_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(addIcon, 0.53f)); + } + + ClockTabPanel(ClockManager clockManager) + { + this.clockManager = clockManager; + + setLayout(new DynamicGridLayout(0, 1, 0, 4)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + rebuild(); + } + + /** + * Clears and recreates the components of this panel. + * This should be done whenever a clock is added or removed. + */ + void rebuild() + { + removeAll(); + clockPanels.clear(); + + add(createHeaderPanel("Timers", "timer", false, e -> clockManager.addTimer())); + + for (Timer timer : clockManager.getTimers()) + { + TimerPanel panel = new TimerPanel(clockManager, timer); + + clockPanels.add(panel); + add(panel); + } + + if (clockManager.getTimers().isEmpty()) + { + add(createInfoPanel("Click the + button to add a timer.")); + } + + add(createHeaderPanel("Stopwatches", "stopwatch", true, e -> clockManager.addStopwatch())); + + for (Stopwatch stopwatch : clockManager.getStopwatches()) + { + StopwatchPanel panel = new StopwatchPanel(clockManager, stopwatch); + + clockPanels.add(panel); + add(panel); + } + + if (clockManager.getStopwatches().isEmpty()) + { + add(createInfoPanel("Click the + button to add a stopwatch.")); + } + + revalidate(); + } + + private JPanel createHeaderPanel(String title, String type, boolean largePadding, ActionListener actionListener) + { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(new EmptyBorder(largePadding ? 11 : 0, 0, 0, 0)); + panel.setBackground(ColorScheme.DARK_GRAY_COLOR); + + JLabel headerLabel = new JLabel(title); + headerLabel.setFont(FontManager.getRunescapeSmallFont()); + panel.add(headerLabel, BorderLayout.CENTER); + + IconButton addButton = new IconButton(ADD_ICON, ADD_ICON_HOVER); + addButton.setPreferredSize(new Dimension(14, 14)); + addButton.setToolTipText("Add a " + type); + addButton.addActionListener(actionListener); + panel.add(addButton, BorderLayout.EAST); + + return panel; + } + + private JPanel createInfoPanel(String text) + { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(new EmptyBorder(7, 8, 6, 8)); + panel.setBackground(ColorScheme.DARK_GRAY_COLOR); + + JLabel infoLabel = new JShadowedLabel(text); + infoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR.darker()); + infoLabel.setFont(FontManager.getRunescapeSmallFont()); + panel.add(infoLabel); + + return panel; + } + + @Override + public int getUpdateInterval() + { + return 1; // 200 milliseconds + } + + @Override + public void update() + { + for (ClockPanel panel : clockPanels) + { + if (panel.getClock().isActive()) + { + panel.updateDisplayInput(); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Stopwatch.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Stopwatch.java new file mode 100644 index 0000000000..5e12f58901 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Stopwatch.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +class Stopwatch extends Clock +{ + // the number of seconds elapsed, as of last updated time + private long elapsed = 0; + + // a list of lap times (recorded as seconds since epoch) + private List laps = new ArrayList<>(); + + Stopwatch(String name) + { + super(name); + } + + @Override + long getDisplayTime() + { + if (!active) + { + return elapsed; + } + + return Math.max(0, elapsed + (Instant.now().getEpochSecond() - lastUpdate)); + } + + @Override + void setDuration(long duration) + { + elapsed = duration; + } + + @Override + boolean start() + { + if (!active) + { + lastUpdate = Instant.now().getEpochSecond(); + active = true; + return true; + } + + return false; + } + + @Override + boolean pause() + { + if (active) + { + active = false; + elapsed = Math.max(0, elapsed + (Instant.now().getEpochSecond() - lastUpdate)); + lastUpdate = Instant.now().getEpochSecond(); + return true; + } + + return false; + } + + void lap() + { + laps.add(getDisplayTime()); + } + + @Override + void reset() + { + active = false; + elapsed = 0; + laps.clear(); + lastUpdate = Instant.now().getEpochSecond(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java new file mode 100644 index 0000000000..99bb8def81 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.List; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.IconButton; + +class StopwatchPanel extends ClockPanel +{ + private static final Color LAP_DATA_COLOR = ColorScheme.LIGHT_GRAY_COLOR.darker(); + + private final JPanel lapsContainer; + private final Stopwatch stopwatch; + + StopwatchPanel(ClockManager clockManager, Stopwatch stopwatch) + { + super(clockManager, stopwatch, "stopwatch", false); + + this.stopwatch = stopwatch; + + lapsContainer = new JPanel(new GridBagLayout()); + lapsContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + rebuildLapList(); + + contentContainer.add(lapsContainer); + + IconButton lapButton = new IconButton(ClockTabPanel.LAP_ICON, ClockTabPanel.LAP_ICON_HOVER); + lapButton.setPreferredSize(new Dimension(16, 14)); + lapButton.setToolTipText("Add lap time"); + + lapButton.addActionListener(e -> + { + stopwatch.lap(); + rebuildLapList(); + clockManager.saveStopwatches(); + }); + + leftActions.add(lapButton); + + IconButton deleteButton = new IconButton(ClockTabPanel.DELETE_ICON, ClockTabPanel.DELETE_ICON_HOVER); + deleteButton.setPreferredSize(new Dimension(16, 14)); + deleteButton.setToolTipText("Delete stopwatch"); + deleteButton.addActionListener(e -> clockManager.removeStopwatch(stopwatch)); + rightActions.add(deleteButton); + } + + @Override + void reset() + { + super.reset(); + rebuildLapList(); + } + + private void rebuildLapList() + { + lapsContainer.removeAll(); + + List laps = stopwatch.getLaps(); + + if (laps.isEmpty()) + { + lapsContainer.setBorder(null); + } + else + { + lapsContainer.setBorder(new EmptyBorder(5, 0, 0, 0)); + + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(4, 5, 3, 5); + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridx = 0; + c.gridy = 0; + + long previousLap = 0; + for (long lap : stopwatch.getLaps()) + { + c.gridx = 0; + lapsContainer.add(createSmallLabel("" + (c.gridy + 1)), c); + + c.gridx = 1; + lapsContainer.add(createSmallLabel(getFormattedDuration(lap - previousLap)), c); + + c.gridx = 2; + lapsContainer.add(createSmallLabel(getFormattedDuration(lap)), c); + + previousLap = lap; + c.gridy++; + } + } + + lapsContainer.revalidate(); + lapsContainer.repaint(); + } + + private JLabel createSmallLabel(String text) + { + JLabel label = new JLabel(text, SwingConstants.CENTER); + label.setFont(FontManager.getRunescapeSmallFont()); + label.setForeground(LAP_DATA_COLOR); + + return label; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Timer.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Timer.java new file mode 100644 index 0000000000..d4e0de6132 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/Timer.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +class Timer extends Clock +{ + // the total number of seconds that the timer should run for + private long duration; + + // the number of seconds remaining on the timer, as of last updated time + private long remaining; + + Timer(String name, long duration) + { + super(name); + this.duration = duration; + this.remaining = duration; + } + + @Override + long getDisplayTime() + { + if (!active) + { + return remaining; + } + + return Math.max(0, remaining - (Instant.now().getEpochSecond() - lastUpdate)); + } + + @Override + boolean start() + { + if (!active && duration > 0) + { + if (remaining <= 0) + { + remaining = duration; + } + lastUpdate = Instant.now().getEpochSecond(); + active = true; + return true; + } + + return false; + } + + @Override + boolean pause() + { + if (active) + { + active = false; + remaining = Math.max(0, remaining - (Instant.now().getEpochSecond() - lastUpdate)); + lastUpdate = Instant.now().getEpochSecond(); + return true; + } + + return false; + } + + @Override + void reset() + { + active = false; + remaining = duration; + lastUpdate = Instant.now().getEpochSecond(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java new file mode 100644 index 0000000000..9f2bf047f8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.clocks; + +import java.awt.Dimension; +import net.runelite.client.ui.components.IconButton; + +class TimerPanel extends ClockPanel +{ + TimerPanel(ClockManager clockManager, Timer timer) + { + super(clockManager, timer, "timer", true); + + IconButton deleteButton = new IconButton(ClockTabPanel.DELETE_ICON, ClockTabPanel.DELETE_ICON_HOVER); + deleteButton.setPreferredSize(new Dimension(16, 14)); + deleteButton.setToolTipText("Delete timer"); + deleteButton.addActionListener(e -> clockManager.removeTimer(timer)); + rightActions.add(deleteButton); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/CropState.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CropState.java similarity index 96% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/CropState.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CropState.java index 90bd66ff73..2d9ffced83 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/CropState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CropState.java @@ -22,7 +22,7 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import java.awt.Color; import lombok.Getter; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingPatch.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java similarity index 95% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingPatch.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java index 7409687cd7..8c24588917 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingPatch.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java @@ -22,7 +22,7 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import lombok.AccessLevel; import lombok.Getter; @@ -34,7 +34,7 @@ import net.runelite.api.Varbits; access = AccessLevel.PACKAGE ) @Getter -public class FarmingPatch +class FarmingPatch { @Setter(AccessLevel.PACKAGE) private FarmingRegion region; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingRegion.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingRegion.java similarity index 97% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingRegion.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingRegion.java index 305a10863a..c768d30c14 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingRegion.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingRegion.java @@ -22,7 +22,7 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import lombok.Getter; import net.runelite.api.Varbits; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java new file mode 100644 index 0000000000..11c1e2dd0b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2018 Abex + * Copyright (c) 2018, Psikoi + * 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.timetracking.farming; + +import com.google.common.base.Strings; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.swing.JLabel; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.vars.Autoweed; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.timetracking.TabContentPanel; +import net.runelite.client.plugins.timetracking.TimeTrackingConfig; +import net.runelite.client.plugins.timetracking.TimeablePanel; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; + +@Slf4j +public class FarmingTabPanel extends TabContentPanel +{ + private final Client client; + private final ItemManager itemManager; + private final ConfigManager configManager; + private final TimeTrackingConfig config; + private final List> patchPanels; + + FarmingTabPanel(Client client, ItemManager itemManager, ConfigManager configManager, + TimeTrackingConfig config, Set patches) + { + this.client = client; + this.itemManager = itemManager; + this.configManager = configManager; + this.config = config; + this.patchPanels = new ArrayList<>(); + + setLayout(new GridBagLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridx = 0; + c.gridy = 0; + + PatchImplementation lastImpl = null; + + boolean first = true; + for (FarmingPatch patch : patches) + { + String title = patch.getRegion().getName() + (Strings.isNullOrEmpty(patch.getName()) ? "" : " (" + patch.getName() + ")"); + TimeablePanel p = new TimeablePanel<>(patch, title, 1); + + /* Show labels to subdivide tabs into sections */ + if (patch.getImplementation() != lastImpl && !Strings.isNullOrEmpty(patch.getImplementation().getName())) + { + JLabel groupLabel = new JLabel(patch.getImplementation().getName()); + + if (first) + { + first = false; + groupLabel.setBorder(new EmptyBorder(4, 0, 0, 0)); + } + else + { + groupLabel.setBorder(new EmptyBorder(15, 0, 0, 0)); + } + + groupLabel.setFont(FontManager.getRunescapeSmallFont()); + + add(groupLabel, c); + c.gridy++; + lastImpl = patch.getImplementation(); + } + + patchPanels.add(p); + add(p, c); + c.gridy++; + + /* This is a weird hack to remove the top border on the first tracker of every tab */ + if (first) + { + first = false; + p.setBorder(null); + } + } + + } + + @Override + public int getUpdateInterval() + { + return 50; // 10 seconds + } + + @Override + public void update() + { + long unixNow = Instant.now().getEpochSecond(); + log.debug("Updating panel with username {}", client.getUsername()); + + boolean autoweed; + { + String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername(); + autoweed = Integer.toString(Autoweed.ON.ordinal()) + .equals(configManager.getConfiguration(group, TimeTrackingConfig.AUTOWEED)); + } + + for (TimeablePanel panel : patchPanels) + { + FarmingPatch patch = panel.getTimeable(); + String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername() + "." + patch.getRegion().getRegionID(); + String key = Integer.toString(patch.getVarbit().getId()); + String storedValue = configManager.getConfiguration(group, key); + long unixTime = 0; + int value = 0; + if (storedValue != null) + { + String[] parts = storedValue.split(":"); + if (parts.length == 2) + { + try + { + value = Integer.parseInt(parts[0]); + unixTime = Long.parseLong(parts[1]); + } + catch (NumberFormatException e) + { + } + } + } + + PatchState state = unixTime <= 0 ? null : patch.getImplementation().forVarbitValue(value); + if (state == null) + { + itemManager.getImage(Produce.WEEDS.getItemID()).addTo(panel.getIcon()); + panel.getIcon().setToolTipText("Unknown state"); + panel.getProgress().setMaximumValue(0); + panel.getProgress().setValue(0); + panel.getProgress().setVisible(false); + panel.getEstimate().setText("Unknown"); + panel.getProgress().setBackground(null); + } + else + { + if (state.getProduce().getItemID() < 0) + { + panel.getIcon().setIcon(null); + panel.getIcon().setToolTipText("Unknown state"); + } + else + { + itemManager.getImage(state.getProduce().getItemID()).addTo(panel.getIcon()); + panel.getIcon().setToolTipText(state.getProduce().getName()); + } + + int stage = state.getStage(); + int stages = state.getStages(); + int tickrate = state.getTickRate() * 60; + + if (autoweed && state.getProduce() == Produce.WEEDS) + { + stage = 0; + stages = 1; + tickrate = 0; + } + if (tickrate > 0) + { + long tickNow = unixNow / tickrate; + long tickTime = unixTime / tickrate; + int delta = (int) (tickNow - tickTime); + + long doneEstimate = ((stages - 1 - stage) + tickTime) * tickrate; + + stage += delta; + if (stage >= stages) + { + stage = stages - 1; + } + + if (doneEstimate < unixNow) + { + panel.getEstimate().setText("Done"); + } + else + { + panel.getEstimate().setText("Done " + getFormattedEstimate(doneEstimate - unixNow, config.estimateRelative())); + } + } + else + { + switch (state.getCropState()) + { + case HARVESTABLE: + panel.getEstimate().setText("Done"); + break; + case GROWING: + if (stage == stages - 1) + { + panel.getEstimate().setText("Done"); + } + else + { + panel.getEstimate().setText("Unknown"); + } + break; + case DISEASED: + panel.getEstimate().setText("Diseased"); + break; + case DEAD: + panel.getEstimate().setText("Dead"); + break; + } + } + + /* Hide any fully grown weeds' progress bar. */ + if (state.getProduce() != Produce.WEEDS + || (state.getProduce() == Produce.WEEDS && !autoweed && stage < stages - 1)) + { + panel.getProgress().setVisible(true); + panel.getProgress().setForeground(state.getCropState().getColor().darker()); + panel.getProgress().setMaximumValue(stages - 1); + panel.getProgress().setValue(stage); + panel.getProgress().update(); + } + else + { + panel.getProgress().setVisible(false); + } + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java new file mode 100644 index 0000000000..d88357c180 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2018 Abex + * 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.timetracking.farming; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.time.Instant; +import java.util.EnumMap; +import java.util.Map; +import net.runelite.api.Client; +import net.runelite.api.Varbits; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.timetracking.Tab; +import net.runelite.client.plugins.timetracking.TimeTrackingConfig; + +@Singleton +public class FarmingTracker +{ + @Deprecated + private static final String OLD_KEY_NAME = "farmingTracker"; + + private final Client client; + private final ItemManager itemManager; + private final ConfigManager configManager; + private final TimeTrackingConfig config; + private final FarmingWorld farmingWorld; + + @Inject + private FarmingTracker(Client client, ItemManager itemManager, ConfigManager configManager, + TimeTrackingConfig config, FarmingWorld farmingWorld) + { + this.client = client; + this.itemManager = itemManager; + this.configManager = configManager; + this.config = config; + this.farmingWorld = farmingWorld; + } + + + /** + * The time at which all patches of a particular type will be ready to be harvested, + * or {@code -1} if we have no data about any patch of the given type. + * + * Each value is set to {@code 0} if all patches of that type have already completed + * when updating the value. + */ + private Map completionTimes = new EnumMap<>(PatchImplementation.class); + + public FarmingTabPanel createTabPanel(Tab tab) + { + return new FarmingTabPanel(client, itemManager, configManager, config, farmingWorld.getTabs().get(tab)); + } + + /** + * Updates tracker data for the current region. Returns true if any data was changed. + */ + public boolean updateData(WorldPoint location) + { + boolean changed = false; + + { + String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername(); + String autoweed = Integer.toString(client.getVar(Varbits.AUTOWEED)); + if (!autoweed.equals(configManager.getConfiguration(group, TimeTrackingConfig.AUTOWEED))) + { + configManager.setConfiguration(group, TimeTrackingConfig.AUTOWEED, autoweed); + changed = true; + } + } + + FarmingRegion region = farmingWorld.getRegions().get(location.getRegionID()); + if (region != null && region.isInBounds(location)) + { + // Write config with new varbits + // timetracking...=: + String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername() + "." + region.getRegionID(); + long unixNow = Instant.now().getEpochSecond(); + for (FarmingPatch patch : region.getPatches()) + { + // Write the config value if it doesn't match what is current, or it is more than 5 minutes old + Varbits varbit = patch.getVarbit(); + String key = Integer.toString(varbit.getId()); + String strVarbit = Integer.toString(client.getVar(varbit)); + String storedValue = configManager.getConfiguration(group, key); + + if (storedValue != null) + { + String[] parts = storedValue.split(":"); + if (parts.length == 2 && parts[0].equals(strVarbit)) + { + long unixTime = 0; + try + { + unixTime = Long.parseLong(parts[1]); + } + catch (NumberFormatException e) + { + // ignored + } + if (unixTime + (5 * 60) > unixNow && unixNow + 30 > unixTime) + { + continue; + } + } + } + + String value = strVarbit + ":" + unixNow; + configManager.setConfiguration(group, key, value); + updateCompletionTime(patch.getImplementation()); + changed = true; + } + } + + return changed; + } + + public void loadCompletionTimes() + { + completionTimes.clear(); + + for (PatchImplementation patchType : PatchImplementation.values()) + { + updateCompletionTime(patchType); + } + } + + /** + * Gets the overall completion time for the given patch type. + * @see #completionTimes + */ + public long getCompletionTime(PatchImplementation patchType) + { + Long completionTime = completionTimes.get(patchType); + return completionTime == null ? -1 : completionTime; + } + + /** + * Updates the overall completion time for the given patch type. + * @see #completionTimes + */ + private void updateCompletionTime(PatchImplementation patchType) + { + long maxCompletionTime = 0; + boolean allUnknown = true; + + for (FarmingPatch patch : farmingWorld.getPatchTypes().get(patchType)) + { + String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername() + "." + patch.getRegion().getRegionID(); + String key = Integer.toString(patch.getVarbit().getId()); + String storedValue = configManager.getConfiguration(group, key); + long unixTime = 0; + int value = 0; + + if (storedValue != null) + { + String[] parts = storedValue.split(":"); + if (parts.length == 2) + { + try + { + value = Integer.parseInt(parts[0]); + unixTime = Long.parseLong(parts[1]); + } + catch (NumberFormatException e) + { + // ignored + } + } + } + + PatchState state = unixTime <= 0 ? null : patch.getImplementation().forVarbitValue(value); + if (state == null || state.getProduce().getItemID() < 0) + { + continue; // unknown state + } + + int tickrate = state.getTickRate() * 60; + int stage = state.getStage(); + int stages = state.getStages(); + + if (state.getProduce() != Produce.WEEDS && state.getProduce() != Produce.SCARECROW) + { + // update max duration if this patch takes longer to grow + if (tickrate > 0) + { + long tickTime = unixTime / tickrate; + long doneEstimate = ((stages - 1 - stage) + tickTime) * tickrate; + maxCompletionTime = Math.max(maxCompletionTime, doneEstimate); + } + else if (state.getCropState() == CropState.GROWING && stage != stages - 1) + { + continue; // unknown state + } + } + + allUnknown = false; + } + + if (allUnknown) + { + completionTimes.put(patchType, -1L); + return; + } + + completionTimes.put(patchType, (maxCompletionTime <= Instant.now().getEpochSecond()) ? 0 : maxCompletionTime); + } + + /** + * Migrates configuration data from {@code "farmingTracker"} key to {@code "timetracking"} key. + * This method should be removed after a reasonable amount of time. + */ + @Deprecated + public void migrateConfiguration() + { + String username = client.getUsername(); + + // migrate autoweed config + { + String oldGroup = OLD_KEY_NAME + "." + username; + String newGroup = TimeTrackingConfig.CONFIG_GROUP + "." + username; + String storedValue = configManager.getConfiguration(oldGroup, TimeTrackingConfig.AUTOWEED); + + if (storedValue != null) + { + configManager.setConfiguration(newGroup, TimeTrackingConfig.AUTOWEED, storedValue); + configManager.unsetConfiguration(oldGroup, TimeTrackingConfig.AUTOWEED); + } + } + + // migrate all saved data in all regions + for (FarmingRegion region : farmingWorld.getRegions().values()) + { + String oldGroup = OLD_KEY_NAME + "." + username + "." + region.getRegionID(); + String newGroup = TimeTrackingConfig.CONFIG_GROUP + "." + username + "." + region.getRegionID(); + + for (Varbits varbit : region.getVarbits()) + { + String key = Integer.toString(varbit.getId()); + String storedValue = configManager.getConfiguration(oldGroup, key); + + if (storedValue != null) + { + configManager.setConfiguration(newGroup, key, storedValue); + configManager.unsetConfiguration(oldGroup, key); + } + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingWorld.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingWorld.java similarity index 95% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingWorld.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingWorld.java index d5079c1696..06f5aa8379 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/FarmingWorld.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingWorld.java @@ -23,12 +23,15 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import com.google.inject.Singleton; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.EnumMap; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -36,9 +39,10 @@ import java.util.TreeSet; import lombok.Getter; import net.runelite.api.Varbits; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.timetracking.Tab; @Singleton -public class FarmingWorld +class FarmingWorld { @Getter private Map regions = new HashMap<>(); @@ -46,12 +50,15 @@ public class FarmingWorld @Getter private Map> tabs = new HashMap<>(); + @Getter + private Map> patchTypes = new EnumMap<>(PatchImplementation.class); + private final Comparator tabSorter = Comparator .comparing(FarmingPatch::getImplementation) .thenComparing((FarmingPatch p) -> p.getRegion().getName()) .thenComparing(FarmingPatch::getName); - public FarmingWorld() + FarmingWorld() { // Some of these patches get updated in multiple regions. // It may be worth it to add a specialization for these patches @@ -242,6 +249,10 @@ public class FarmingWorld tabs .computeIfAbsent(p.getImplementation().getTab(), k -> new TreeSet<>(tabSorter)) .add(p); + + patchTypes + .computeIfAbsent(p.getImplementation(), k -> new ArrayList<>()) + .add(p); } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/PatchImplementation.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/PatchImplementation.java similarity index 99% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/PatchImplementation.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/PatchImplementation.java index 89f4c40274..0a4000ac8e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/PatchImplementation.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/PatchImplementation.java @@ -22,10 +22,11 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import lombok.Getter; import lombok.RequiredArgsConstructor; +import net.runelite.client.plugins.timetracking.Tab; @RequiredArgsConstructor @Getter @@ -733,7 +734,7 @@ public enum PatchImplementation return null; } }, - FLOWER(Tab.HERB, "Flowers") + FLOWER(Tab.FLOWER, "") { @Override PatchState forVarbitValue(int value) @@ -1205,7 +1206,7 @@ public enum PatchImplementation return null; } }, - FRUIT_TREE(Tab.FRUIT_TREE, "Fruit trees") + FRUIT_TREE(Tab.FRUIT_TREE, "") { @Override PatchState forVarbitValue(int value) @@ -1454,7 +1455,7 @@ public enum PatchImplementation return null; } }, - HOPS(Tab.SPECIAL, "Hops") + HOPS(Tab.HOPS, "") { @Override PatchState forVarbitValue(int value) @@ -2267,7 +2268,7 @@ public enum PatchImplementation return null; } }, - GRAPES(Tab.BUSH, "Grapes") + GRAPES(Tab.GRAPE, "") { @Override PatchState forVarbitValue(int value) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/PatchState.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/PatchState.java similarity index 79% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/PatchState.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/PatchState.java index c27e13d6c6..2857882b3f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/PatchState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/PatchState.java @@ -22,14 +22,32 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import lombok.Value; @Value -public class PatchState +class PatchState { private final Produce produce; private final CropState cropState; private final int stage; + + int getStages() + { + return cropState == CropState.HARVESTABLE ? produce.getHarvestStages() : produce.getStages(); + } + + int getTickRate() + { + switch (cropState) + { + case HARVESTABLE: + return produce.getRegrowTickrate(); + case GROWING: + return produce.getTickrate(); + default: + return 0; + } + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/Produce.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java similarity index 97% rename from runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/Produce.java rename to runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java index 867105fee7..96d4c6e189 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/farmingtracker/Produce.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java @@ -23,7 +23,7 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -127,7 +127,7 @@ public enum Produce */ private final int tickrate; /** - * How many states this crop has during groth. Typically tickcount+1 + * How many states this crop has during growth. Typically tickcount+1 */ private final int stages; /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouse.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouse.java new file mode 100644 index 0000000000..009a2bd7e6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouse.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.hunter; + +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.runelite.api.ItemID; + +@AllArgsConstructor +@Getter +enum BirdHouse +{ + NORMAL("Bird House", ItemID.BIRD_HOUSE), + OAK("Oak Bird House", ItemID.OAK_BIRD_HOUSE), + WILLOW("Willow Bird House", ItemID.WILLOW_BIRD_HOUSE), + TEAK("Teak Bird House", ItemID.TEAK_BIRD_HOUSE), + MAPLE("Maple Bird House", ItemID.MAPLE_BIRD_HOUSE), + MAHOGANY("Mahogany Bird House", ItemID.MAHOGANY_BIRD_HOUSE), + YEW("Yew Bird House", ItemID.YEW_BIRD_HOUSE), + MAGIC("Magic Bird House", ItemID.MAGIC_BIRD_HOUSE), + REDWOOD("Redwood Bird House", ItemID.REDWOOD_BIRD_HOUSE); + + private final String name; + private final int itemID; + + /** + * Gets the {@code BirdHouse} corresponding to the given {@code VarPlayer} value. + */ + @Nullable + static BirdHouse fromVarpValue(int varp) + { + int index = (varp - 1) / 3; + + if (varp <= 0 || index >= values().length) + { + return null; + } + + return values()[index]; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseData.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseData.java new file mode 100644 index 0000000000..f5af4893ef --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseData.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.hunter; + +import lombok.Value; + +/** + * Contains data about the state of a particular {@link BirdHouseSpace}, at a particular point in time. + */ +@Value +class BirdHouseData +{ + private BirdHouseSpace space; + private int varp; + private long timestamp; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseSpace.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseSpace.java new file mode 100644 index 0000000000..635ce2a4e3 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseSpace.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.hunter; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.runelite.api.VarPlayer; + +@AllArgsConstructor +@Getter +enum BirdHouseSpace +{ + MEADOW_NORTH("Mushroom Meadow (North)", VarPlayer.BIRD_HOUSE_MEADOW_NORTH), + MEADOW_SOUTH("Mushroom Meadow (South)", VarPlayer.BIRD_HOUSE_MEADOW_SOUTH), + VALLEY_NORTH("Verdant Valley (Northeast)", VarPlayer.BIRD_HOUSE_VALLEY_NORTH), + VALLEY_SOUTH("Verdant Valley (Southwest)", VarPlayer.BIRD_HOUSE_VALLEY_SOUTH); + + private final String name; + private final VarPlayer varp; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseState.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseState.java new file mode 100644 index 0000000000..8e99d92541 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseState.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.hunter; + +import java.awt.Color; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.runelite.client.ui.ColorScheme; + +@AllArgsConstructor +@Getter +enum BirdHouseState +{ + SEEDED(ColorScheme.PROGRESS_COMPLETE_COLOR), + BUILT(ColorScheme.PROGRESS_INPROGRESS_COLOR), + EMPTY(ColorScheme.MEDIUM_GRAY_COLOR), + UNKNOWN(ColorScheme.MEDIUM_GRAY_COLOR); + + private final Color color; + + /** + * Gets the {@code BirdHouseState} corresponding to the given {@code VarPlayer} value. + */ + static BirdHouseState fromVarpValue(int varp) + { + if (varp < 0 || varp > BirdHouse.values().length * 3) + { + return UNKNOWN; + } + else if (varp == 0) + { + return EMPTY; + } + else if (varp % 3 == 0) + { + return SEEDED; + } + else + { + return BUILT; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseTabPanel.java new file mode 100644 index 0000000000..a43a9e6760 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseTabPanel.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2018 Abex + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.hunter; + +import java.awt.Color; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import net.runelite.api.ItemID; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.timetracking.TabContentPanel; +import net.runelite.client.plugins.timetracking.TimeTrackingConfig; +import net.runelite.client.plugins.timetracking.TimeablePanel; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; + +public class BirdHouseTabPanel extends TabContentPanel +{ + private static final Color COMPLETED_COLOR = ColorScheme.PROGRESS_COMPLETE_COLOR.darker(); + + private final ItemManager itemManager; + private final BirdHouseTracker birdHouseTracker; + private final TimeTrackingConfig config; + private final List> spacePanels; + + BirdHouseTabPanel(ItemManager itemManager, BirdHouseTracker birdHouseTracker, TimeTrackingConfig config) + { + this.itemManager = itemManager; + this.birdHouseTracker = birdHouseTracker; + this.config = config; + this.spacePanels = new ArrayList<>(); + + setLayout(new DynamicGridLayout(0, 1, 0, 0)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + boolean first = true; + for (BirdHouseSpace space : BirdHouseSpace.values()) + { + TimeablePanel panel = new TimeablePanel<>(space, space.getName(), BirdHouseTracker.BIRD_HOUSE_DURATION); + + spacePanels.add(panel); + add(panel); + + // remove the top border on the first panel + if (first) + { + first = false; + panel.setBorder(null); + } + } + } + + @Override + public int getUpdateInterval() + { + return 50; // 10 seconds + } + + @Override + public void update() + { + long unixNow = Instant.now().getEpochSecond(); + + for (TimeablePanel panel : spacePanels) + { + BirdHouseSpace space = panel.getTimeable(); + BirdHouseData data = birdHouseTracker.getBirdHouseData().get(space); + int value = -1; + long startTime = 0; + + if (data != null) + { + value = data.getVarp(); + startTime = data.getTimestamp(); + } + + BirdHouse birdHouse = BirdHouse.fromVarpValue(value); + BirdHouseState state = BirdHouseState.fromVarpValue(value); + + if (birdHouse == null) + { + itemManager.getImage(ItemID.FEATHER).addTo(panel.getIcon()); + panel.getProgress().setVisible(false); + } + else + { + itemManager.getImage(birdHouse.getItemID()).addTo(panel.getIcon()); + panel.getIcon().setToolTipText(birdHouse.getName()); + panel.getProgress().setVisible(true); + } + + panel.getProgress().setForeground(state.getColor().darker()); + + switch (state) + { + case EMPTY: + panel.getIcon().setToolTipText("Empty"); + panel.getEstimate().setText("Empty"); + break; + case BUILT: + panel.getProgress().setValue(0); + panel.getEstimate().setText("Built"); + break; + case SEEDED: + long remainingTime = startTime + BirdHouseTracker.BIRD_HOUSE_DURATION - unixNow; + if (remainingTime <= 0) + { + panel.getProgress().setValue(BirdHouseTracker.BIRD_HOUSE_DURATION); + panel.getProgress().setForeground(COMPLETED_COLOR); + panel.getEstimate().setText("Done"); + } + else + { + panel.getProgress().setValue((int) (BirdHouseTracker.BIRD_HOUSE_DURATION - remainingTime)); + panel.getEstimate().setText("Done " + getFormattedEstimate(remainingTime, config.estimateRelative())); + } + break; + default: + panel.getIcon().setToolTipText("Unknown state"); + panel.getEstimate().setText("Unknown"); + break; + } + + panel.getProgress().update(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseTracker.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseTracker.java new file mode 100644 index 0000000000..bcc52c3349 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/hunter/BirdHouseTracker.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2018 Abex + * Copyright (c) 2018, Daniel Teo + * 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.timetracking.hunter; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.Notifier; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.timetracking.TimeTrackingConfig; + +@Singleton +public class BirdHouseTracker +{ + // average time taken to harvest 10 birds, in seconds + static final int BIRD_HOUSE_DURATION = (int) Duration.ofMinutes(50).getSeconds(); + + private static ImmutableSet FOSSIL_ISLAND_REGIONS = ImmutableSet.of(14650, 14651, 14652, 14906, 14907, 15162, 15163); + + private final Client client; + private final ItemManager itemManager; + private final ConfigManager configManager; + private final TimeTrackingConfig config; + private final Notifier notifier; + + @Getter(AccessLevel.PACKAGE) + private final ConcurrentMap birdHouseData = new ConcurrentHashMap<>(); + + @Inject + private BirdHouseTracker(Client client, ItemManager itemManager, ConfigManager configManager, + TimeTrackingConfig config, Notifier notifier) + { + this.client = client; + this.itemManager = itemManager; + this.configManager = configManager; + this.config = config; + this.notifier = notifier; + } + + /** + * The time at which all the bird houses will be ready to be dismantled, + * or {@code -1} if we have no data about any of the bird house spaces. + * + * This is set to {@code 0} if the bird houses have already completed + * when updating it. + */ + @Getter + private long completionTime = -1; + + public BirdHouseTabPanel createBirdHouseTabPanel() + { + return new BirdHouseTabPanel(itemManager, this, config); + } + + public void loadFromConfig() + { + birdHouseData.clear(); + + final String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername() + "." + TimeTrackingConfig.BIRD_HOUSE; + + for (BirdHouseSpace space : BirdHouseSpace.values()) + { + String key = Integer.toString(space.getVarp().getId()); + String storedValue = configManager.getConfiguration(group, key); + + if (storedValue != null) + { + String[] parts = storedValue.split(":"); + if (parts.length == 2) + { + try + { + int varp = Integer.parseInt(parts[0]); + long timestamp = Long.parseLong(parts[1]); + birdHouseData.put(space, new BirdHouseData(space, varp, timestamp)); + } + catch (NumberFormatException e) + { + // ignored + } + } + } + } + + updateCompletionTime(); + } + + /** + * Updates tracker data if player is within range of any bird house. Returns true if any data was changed. + */ + public boolean updateData(WorldPoint location) + { + boolean changed = false; + + if (FOSSIL_ISLAND_REGIONS.contains(location.getRegionID())) + { + final Map newData = new HashMap<>(); + final long currentTime = Instant.now().getEpochSecond(); + int removalCount = 0; + + for (BirdHouseSpace space : BirdHouseSpace.values()) + { + int varp = client.getVar(space.getVarp()); + BirdHouseData oldData = birdHouseData.get(space); + int oldVarp = oldData == null ? -1 : oldData.getVarp(); + + // update data if there isn't one, or if the varp doesn't match + if (varp != oldVarp) + { + newData.put(space, new BirdHouseData(space, varp, currentTime)); + changed = true; + } + + if (varp <= 0 && oldVarp > 0) + { + removalCount++; + } + } + + // Prevent the resetting of bird house data that could occur if the varps have not been updated yet + // after the player enters the region. We assume that players would generally have 3 or 4 bird houses + // built at any time, and that dropping from 3/4 to 0 built bird houses is not normally possible. + if (removalCount > 2) + { + return false; + } + + if (changed) + { + birdHouseData.putAll(newData); + updateCompletionTime(); + saveToConfig(newData); + } + } + + return changed; + } + + /** + * Checks if the bird houses have become ready to be dismantled, + * and sends a notification if required. + */ + public boolean checkCompletion() + { + if (completionTime > 0 && completionTime < Instant.now().getEpochSecond()) + { + completionTime = 0; + + if (config.birdHouseNotification()) + { + notifier.notify("Your bird houses are ready to be dismantled."); + } + + return true; + } + + return false; + } + + /** + * Updates the overall completion time of the bird houses. + * @see #completionTime + */ + private void updateCompletionTime() + { + if (birdHouseData.isEmpty()) + { + completionTime = -1; + return; + } + + long maxCompletionTime = 0; + for (BirdHouseData data : birdHouseData.values()) + { + if (BirdHouseState.fromVarpValue(data.getVarp()) == BirdHouseState.SEEDED) + { + maxCompletionTime = Math.max(maxCompletionTime, data.getTimestamp() + BIRD_HOUSE_DURATION); + } + } + + completionTime = (maxCompletionTime <= Instant.now().getEpochSecond()) ? 0 : maxCompletionTime; + } + + private void saveToConfig(Map updatedData) + { + final String group = TimeTrackingConfig.CONFIG_GROUP + "." + client.getUsername() + "." + TimeTrackingConfig.BIRD_HOUSE; + + for (BirdHouseData data : updatedData.values()) + { + String key = Integer.toString(data.getSpace().getVarp().getId()); + configManager.setConfiguration(group, key, data.getVarp() + ":" + data.getTimestamp()); + } + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/farmingtracker/farming.png b/runelite-client/src/main/resources/net/runelite/client/plugins/farmingtracker/farming.png deleted file mode 100644 index 5b1c5dff73..0000000000 Binary files a/runelite-client/src/main/resources/net/runelite/client/plugins/farmingtracker/farming.png and /dev/null differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/add_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/add_icon.png new file mode 100644 index 0000000000..022dbff0ef Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/add_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/arrow_right.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/arrow_right.png new file mode 100644 index 0000000000..92048f0c78 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/arrow_right.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/delete_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/delete_icon.png new file mode 100644 index 0000000000..ee9ea58a18 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/delete_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/lap_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/lap_icon.png new file mode 100644 index 0000000000..6b7acbb25c Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/lap_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/pause_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/pause_icon.png new file mode 100644 index 0000000000..45d8b770ac Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/pause_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/reset_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/reset_icon.png new file mode 100644 index 0000000000..d439c15dc9 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/reset_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/start_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/start_icon.png new file mode 100644 index 0000000000..cf722905e9 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/start_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/watch.png b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/watch.png new file mode 100644 index 0000000000..e418efc732 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/timetracking/watch.png differ diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/farmingtracker/FarmingWorldTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/FarmingWorldTest.java similarity index 96% rename from runelite-client/src/test/java/net/runelite/client/plugins/farmingtracker/FarmingWorldTest.java rename to runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/FarmingWorldTest.java index 918a24a7f5..05a45ffe52 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/farmingtracker/FarmingWorldTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/FarmingWorldTest.java @@ -21,7 +21,7 @@ * 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.farmingtracker; + */package net.runelite.client.plugins.timetracking.farming; import org.junit.Test; diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/farmingtracker/PatchImplementationTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/PatchImplementationTest.java similarity index 98% rename from runelite-client/src/test/java/net/runelite/client/plugins/farmingtracker/PatchImplementationTest.java rename to runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/PatchImplementationTest.java index 8023958af8..ef6f73b94f 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/farmingtracker/PatchImplementationTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/PatchImplementationTest.java @@ -22,7 +22,7 @@ * (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.farmingtracker; +package net.runelite.client.plugins.timetracking.farming; import java.util.HashMap; import java.util.Map;