timetracking: add compost tracking

This commit is contained in:
LlemonDuck
2022-05-07 14:19:13 -07:00
committed by GitHub
parent bb1f49b0b4
commit 0500f94830
11 changed files with 758 additions and 9 deletions

View File

@@ -773,6 +773,7 @@ public final class WidgetID
static class LunarSpellBook
{
static final int LUNAR_HOME_TELEPORT = 101;
static final int FERTILE_SOIL = 126;
}
static class ArceuusSpellBook

View File

@@ -505,6 +505,7 @@ public enum WidgetInfo
SPELL_ARCEUUS_HOME_TELEPORT(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.ArceuusSpellBook.ARCEUUS_HOME_TELEPORT),
SPELL_KOUREND_HOME_TELEPORT(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.StandardSpellBook.KOUREND_HOME_TELEPORT),
SPELL_CATHERBY_HOME_TELEPORT(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.StandardSpellBook.CATHERBY_HOME_TELEPORT),
SPELL_LUNAR_FERTILE_SOIL(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.LunarSpellBook.FERTILE_SOIL),
PVP_WILDERNESS_SKULL_CONTAINER(WidgetID.PVP_GROUP_ID, WidgetID.Pvp.WILDERNESS_SKULL_CONTAINER),
PVP_SKULL_CONTAINER(WidgetID.PVP_GROUP_ID, WidgetID.Pvp.SKULL_CONTAINER),

View File

@@ -43,6 +43,7 @@ public interface TimeTrackingConfig extends Config
String PREFER_SOONEST = "preferSoonest";
String NOTIFY = "notify";
String BIRDHOUSE_NOTIFY = "birdHouseNotification";
String COMPOST = "compost";
@ConfigItem(
keyName = "timeFormatMode",

View File

@@ -44,6 +44,7 @@ import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.api.widgets.WidgetModalMode;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.RuneScapeProfileChanged;
@@ -54,6 +55,7 @@ import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.PREFER
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.CompostTracker;
import net.runelite.client.plugins.timetracking.farming.FarmingContractManager;
import net.runelite.client.plugins.timetracking.farming.FarmingTracker;
import net.runelite.client.plugins.timetracking.hunter.BirdHouseTracker;
@@ -77,6 +79,12 @@ public class TimeTrackingPlugin extends Plugin
@Inject
private Client client;
@Inject
private EventBus eventBus;
@Inject
private CompostTracker compostTracker;
@Inject
private FarmingTracker farmingTracker;
@@ -125,6 +133,8 @@ public class TimeTrackingPlugin extends Plugin
birdHouseTracker.loadFromConfig();
farmingTracker.loadCompletionTimes();
eventBus.register(compostTracker);
final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "watch.png");
panel = injector.getInstance(TimeTrackingPanel.class);
@@ -148,6 +158,8 @@ public class TimeTrackingPlugin extends Plugin
lastTickLocation = null;
lastTickPostLogin = false;
eventBus.unregister(compostTracker);
if (panelUpdateFuture != null)
{
panelUpdateFuture.cancel(true);

View File

@@ -29,8 +29,11 @@ import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.border.EmptyBorder;
@@ -48,9 +51,20 @@ public class TimeablePanel<T> extends JPanel
{
private static final ImageIcon NOTIFY_ICON = new ImageIcon(ImageUtil.loadImageResource(TimeTrackingPlugin.class, "notify_icon.png"));
private static final ImageIcon NOTIFY_SELECTED_ICON = new ImageIcon(ImageUtil.loadImageResource(TimeTrackingPlugin.class, "notify_selected_icon.png"));
private static final Rectangle OVERLAY_ICON_BOUNDS;
static
{
int width = Constants.ITEM_SPRITE_WIDTH * 2 / 3;
int height = Constants.ITEM_SPRITE_HEIGHT * 2 / 3;
int x = Constants.ITEM_SPRITE_WIDTH - width;
int y = Constants.ITEM_SPRITE_HEIGHT - height;
OVERLAY_ICON_BOUNDS = new Rectangle(x, y, width, height);
}
private final T timeable;
private final JLabel icon = new JLabel();
private final JLabel overlayIcon = new JLabel();
private final JLabel farmingContractIcon = new JLabel();
private final JToggleButton notifyButton = new JToggleButton();
private final JLabel estimate = new JLabel();
@@ -70,6 +84,7 @@ public class TimeablePanel<T> extends JPanel
topContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR);
icon.setMinimumSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT));
overlayIcon.setMinimumSize(OVERLAY_ICON_BOUNDS.getSize());
farmingContractIcon.setMinimumSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT));
JPanel infoPanel = new JPanel();
@@ -105,8 +120,15 @@ public class TimeablePanel<T> extends JPanel
iconPanel.add(notifyPanel, BorderLayout.EAST);
iconPanel.add(farmingContractIcon, BorderLayout.WEST);
JLayeredPane layeredIconPane = new JLayeredPane();
layeredIconPane.setPreferredSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT));
layeredIconPane.add(icon, Integer.valueOf(0));
layeredIconPane.add(overlayIcon, Integer.valueOf(1));
icon.setBounds(0, 0, Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT);
overlayIcon.setBounds(OVERLAY_ICON_BOUNDS);
topContainer.add(iconPanel, BorderLayout.EAST);
topContainer.add(icon, BorderLayout.WEST);
topContainer.add(layeredIconPane, BorderLayout.WEST);
topContainer.add(infoPanel, BorderLayout.CENTER);
progress.setValue(0);
@@ -115,4 +137,21 @@ public class TimeablePanel<T> extends JPanel
add(topContainer, BorderLayout.NORTH);
add(progress, BorderLayout.SOUTH);
}
public void setOverlayIconImage(BufferedImage overlayImg)
{
if (overlayImg == null)
{
overlayIcon.setIcon(null);
return;
}
if (OVERLAY_ICON_BOUNDS.width != overlayImg.getWidth() || OVERLAY_ICON_BOUNDS.height != overlayImg.getHeight())
{
overlayImg = ImageUtil.resizeImage(overlayImg, OVERLAY_ICON_BOUNDS.width, OVERLAY_ICON_BOUNDS.height);
}
overlayIcon.setIcon(new ImageIcon(overlayImg));
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 LlemonDuck
* 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 lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.runelite.api.ItemID;
@RequiredArgsConstructor
@Getter
public enum CompostState
{
COMPOST(ItemID.COMPOST),
SUPERCOMPOST(ItemID.SUPERCOMPOST),
ULTRACOMPOST(ItemID.ULTRACOMPOST),
;
private final int itemId;
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright (c) 2022 LlemonDuck
* 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.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.ItemID;
import net.runelite.api.ObjectComposition;
import net.runelite.api.Tile;
import net.runelite.api.annotations.Varbit;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.timetracking.TimeTrackingConfig;
@Singleton
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class CompostTracker
{
@Value
@VisibleForTesting
static class PendingCompost
{
Instant timeout;
WorldPoint patchLocation;
FarmingPatch farmingPatch;
}
private static final Duration COMPOST_ACTION_TIMEOUT = Duration.ofSeconds(30);
private static final Pattern COMPOST_USED_ON_PATCH = Pattern.compile(
"You treat the .+ with (?<compostType>ultra|super|)compost\\.");
private static final Pattern FERTILE_SOIL_CAST = Pattern.compile(
"The .+ has been treated with (?<compostType>ultra|super|)compost\\.");
private static final Pattern ALREADY_TREATED = Pattern.compile(
"This .+ has already been (treated|fertilised) with (?<compostType>ultra|super|)compost(?: - the spell can't make it any more fertile)?\\.");
private static final Pattern INSPECT_PATCH = Pattern.compile(
"This is an? .+\\. The soil has been treated with (?<compostType>ultra|super|)compost\\..*");
private static final ImmutableSet<Integer> COMPOST_ITEMS = ImmutableSet.of(
ItemID.COMPOST,
ItemID.SUPERCOMPOST,
ItemID.ULTRACOMPOST,
ItemID.BOTTOMLESS_COMPOST_BUCKET_22997
);
private final Client client;
private final FarmingWorld farmingWorld;
private final ConfigManager configManager;
@VisibleForTesting
final Map<FarmingPatch, PendingCompost> pendingCompostActions = new HashMap<>();
private static String configKey(FarmingPatch fp)
{
return fp.configKey() + "." + TimeTrackingConfig.COMPOST;
}
public void setCompostState(FarmingPatch fp, CompostState state)
{
log.debug("Storing compost state [{}] for patch [{}]", state, fp);
if (state == null)
{
configManager.unsetRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, configKey(fp));
}
else
{
configManager.setRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, configKey(fp), state);
}
}
public CompostState getCompostState(FarmingPatch fp)
{
return configManager.getRSProfileConfiguration(
TimeTrackingConfig.CONFIG_GROUP,
configKey(fp),
CompostState.class
);
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked e)
{
if (!isCompostAction(e))
{
return;
}
ObjectComposition patchDef = client.getObjectDefinition(e.getId());
WorldPoint actionLocation = WorldPoint.fromScene(client, e.getParam0(), e.getParam1(), client.getPlane());
FarmingPatch targetPatch = farmingWorld.getRegionsForLocation(actionLocation)
.stream()
.flatMap(fr -> Arrays.stream(fr.getPatches()))
.filter(fp -> fp.getVarbit() == patchDef.getVarbitId())
.findFirst()
.orElse(null);
if (targetPatch == null)
{
return;
}
log.debug("Storing pending compost action for patch [{}]", targetPatch);
PendingCompost pc = new PendingCompost(
Instant.now().plus(COMPOST_ACTION_TIMEOUT),
actionLocation,
targetPatch
);
pendingCompostActions.put(targetPatch, pc);
}
private boolean isCompostAction(MenuOptionClicked e)
{
switch (e.getMenuAction())
{
case WIDGET_TARGET_ON_GAME_OBJECT:
Widget w = client.getSelectedWidget();
assert w != null;
return COMPOST_ITEMS.contains(w.getItemId()) || w.getId() == WidgetInfo.SPELL_LUNAR_FERTILE_SOIL.getPackedId();
case GAME_OBJECT_FIRST_OPTION:
case GAME_OBJECT_SECOND_OPTION:
case GAME_OBJECT_THIRD_OPTION:
case GAME_OBJECT_FOURTH_OPTION:
case GAME_OBJECT_FIFTH_OPTION:
return "Inspect".equals(e.getMenuOption());
default:
return false;
}
}
@Subscribe
public void onChatMessage(ChatMessage e)
{
if (e.getType() != ChatMessageType.GAMEMESSAGE && e.getType() != ChatMessageType.SPAM)
{
return;
}
CompostState compostUsed = determineCompostUsed(e.getMessage());
if (compostUsed == null)
{
return;
}
this.expirePendingActions();
pendingCompostActions.values()
.stream()
.filter(this::playerIsBesidePatch)
.findFirst()
.ifPresent(pc ->
{
setCompostState(pc.getFarmingPatch(), compostUsed);
pendingCompostActions.remove(pc.getFarmingPatch());
});
}
@Subscribe
public void onGameStateChanged(GameStateChanged e)
{
switch (e.getGameState())
{
case LOGGED_IN:
case LOADING:
return;
default:
pendingCompostActions.clear();
}
}
private boolean playerIsBesidePatch(PendingCompost pendingCompost)
{
// find gameobject instance in scene
// it is possible that the scene has reloaded between use and action occurring so we use worldpoint
// instead of storing scene coords in the menuoptionclicked event
LocalPoint localPatchLocation = LocalPoint.fromWorld(client, pendingCompost.getPatchLocation());
if (localPatchLocation == null)
{
return false;
}
@Varbit int patchVarb = pendingCompost.getFarmingPatch().getVarbit();
Tile patchTile = client.getScene()
.getTiles()[client.getPlane()][localPatchLocation.getSceneX()][localPatchLocation.getSceneY()];
GameObject patchObject = null;
for (GameObject go : patchTile.getGameObjects())
{
if (go != null && client.getObjectDefinition(go.getId()).getVarbitId() == patchVarb)
{
patchObject = go;
break;
}
}
assert patchObject != null;
// player coords
final WorldPoint playerPos = client.getLocalPlayer().getWorldLocation();
final int playerX = playerPos.getX();
final int playerY = playerPos.getY();
// patch coords
final WorldPoint patchBase = pendingCompost.getPatchLocation();
final int minX = patchBase.getX();
final int minY = patchBase.getY();
final int maxX = minX + patchObject.sizeX() - 1;
final int maxY = minY + patchObject.sizeY() - 1;
// player should be within one tile of these coords
return playerX >= (minX - 1) && playerX <= (maxX + 1) && playerY >= (minY - 1) && playerY <= (maxY + 1);
}
private void expirePendingActions()
{
pendingCompostActions.values().removeIf(e -> Instant.now().isAfter(e.getTimeout()));
}
@VisibleForTesting
static CompostState determineCompostUsed(String chatMessage)
{
if (!chatMessage.contains("compost"))
{
return null;
}
Matcher matcher;
if ((matcher = COMPOST_USED_ON_PATCH.matcher(chatMessage)).matches() ||
(matcher = FERTILE_SOIL_CAST.matcher(chatMessage)).matches() ||
(matcher = ALREADY_TREATED.matcher(chatMessage)).matches() ||
(matcher = INSPECT_PATCH.matcher(chatMessage)).matches())
{
String compostGroup = matcher.group("compostType");
switch (compostGroup)
{
case "ultra":
return CompostState.ULTRACOMPOST;
case "super":
return CompostState.SUPERCOMPOST;
default:
return CompostState.COMPOST;
}
}
return null;
}
}

View File

@@ -28,6 +28,7 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import net.runelite.api.annotations.Varbit;
import net.runelite.client.plugins.timetracking.TimeTrackingConfig;
@@ -35,13 +36,17 @@ import net.runelite.client.plugins.timetracking.TimeTrackingConfig;
access = AccessLevel.PACKAGE
)
@Getter
@ToString(onlyExplicitlyIncluded = true)
class FarmingPatch
{
@Setter(AccessLevel.PACKAGE)
@ToString.Include
private FarmingRegion region;
@ToString.Include
private final String name;
@Getter(onMethod_ = {@Varbit})
private final int varbit;
@ToString.Include
private final PatchImplementation implementation;
String configKey()

View File

@@ -43,10 +43,12 @@ 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;
import net.runelite.client.util.AsyncBufferedImage;
public class FarmingTabPanel extends TabContentPanel
{
private final FarmingTracker farmingTracker;
private final CompostTracker compostTracker;
private final ItemManager itemManager;
private final ConfigManager configManager;
private final TimeTrackingConfig config;
@@ -55,6 +57,7 @@ public class FarmingTabPanel extends TabContentPanel
FarmingTabPanel(
FarmingTracker farmingTracker,
CompostTracker compostTracker,
ItemManager itemManager,
ConfigManager configManager,
TimeTrackingConfig config,
@@ -63,6 +66,7 @@ public class FarmingTabPanel extends TabContentPanel
)
{
this.farmingTracker = farmingTracker;
this.compostTracker = compostTracker;
this.itemManager = itemManager;
this.configManager = configManager;
this.config = config;
@@ -131,7 +135,6 @@ public class FarmingTabPanel extends TabContentPanel
p.setBorder(null);
}
}
}
@Override
@@ -150,10 +153,26 @@ public class FarmingTabPanel extends TabContentPanel
FarmingPatch patch = panel.getTimeable();
PatchPrediction prediction = farmingTracker.predictPatch(patch);
CompostState compostState = compostTracker.getCompostState(patch);
String compostTooltip = "";
if (compostState != null)
{
AsyncBufferedImage compostImg = itemManager.getImage(compostState.getItemId());
Runnable compostOverlayRunnable = () -> panel.setOverlayIconImage(compostImg);
compostImg.onLoaded(compostOverlayRunnable);
compostOverlayRunnable.run();
compostTooltip = " with " + compostState.name().toLowerCase();
}
else
{
panel.setOverlayIconImage(null);
}
if (prediction == null)
{
itemManager.getImage(Produce.WEEDS.getItemID()).addTo(panel.getIcon());
panel.getIcon().setToolTipText("Unknown state");
panel.getIcon().setToolTipText("Unknown state" + compostTooltip);
panel.getProgress().setMaximumValue(0);
panel.getProgress().setValue(0);
panel.getProgress().setVisible(false);
@@ -165,12 +184,12 @@ public class FarmingTabPanel extends TabContentPanel
if (prediction.getProduce().getItemID() < 0)
{
panel.getIcon().setIcon(null);
panel.getIcon().setToolTipText("Unknown state");
panel.getIcon().setToolTipText("Unknown state" + compostTooltip);
}
else
{
itemManager.getImage(prediction.getProduce().getItemID()).addTo(panel.getIcon());
panel.getIcon().setToolTipText(prediction.getProduce().getName());
panel.getIcon().setToolTipText(prediction.getProduce().getName() + compostTooltip);
}
switch (prediction.getCropState())

View File

@@ -63,6 +63,7 @@ public class FarmingTracker
private final TimeTrackingConfig config;
private final FarmingWorld farmingWorld;
private final Notifier notifier;
private final CompostTracker compostTracker;
private final Map<Tab, SummaryState> summaries = new EnumMap<>(Tab.class);
@@ -78,7 +79,7 @@ public class FarmingTracker
private boolean firstNotifyCheck = true;
@Inject
private FarmingTracker(Client client, ItemManager itemManager, ConfigManager configManager, TimeTrackingConfig config, FarmingWorld farmingWorld, Notifier notifier)
private FarmingTracker(Client client, ItemManager itemManager, ConfigManager configManager, TimeTrackingConfig config, FarmingWorld farmingWorld, Notifier notifier, CompostTracker compostTracker)
{
this.client = client;
this.itemManager = itemManager;
@@ -86,11 +87,12 @@ public class FarmingTracker
this.config = config;
this.farmingWorld = farmingWorld;
this.notifier = notifier;
this.compostTracker = compostTracker;
}
public FarmingTabPanel createTabPanel(Tab tab, FarmingContractManager farmingContractManager)
{
return new FarmingTabPanel(this, itemManager, configManager, config, farmingWorld.getTabs().get(tab), farmingContractManager);
return new FarmingTabPanel(this, compostTracker, itemManager, configManager, config, farmingWorld.getTabs().get(tab), farmingContractManager);
}
/**
@@ -148,6 +150,12 @@ public class FarmingTracker
String strVarbit = Integer.toString(client.getVarbitValue(varbit));
String storedValue = configManager.getRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, key);
PatchState currentPatchState = patch.getImplementation().forVarbitValue(client.getVarbitValue(varbit));
if (currentPatchState == null)
{
continue;
}
if (storedValue != null)
{
String[] parts = storedValue.split(":");
@@ -172,9 +180,8 @@ public class FarmingTracker
else if (!newRegionLoaded && timeSinceModalClose > 1)
{
PatchState previousPatchState = patch.getImplementation().forVarbitValue(Integer.parseInt(parts[0]));
PatchState currentPatchState = patch.getImplementation().forVarbitValue(client.getVarbitValue(varbit));
if (previousPatchState == null || currentPatchState == null)
if (previousPatchState == null)
{
continue;
}
@@ -217,6 +224,11 @@ public class FarmingTracker
}
}
if (currentPatchState.getCropState() == CropState.DEAD || currentPatchState.getCropState() == CropState.HARVESTABLE)
{
compostTracker.setCompostState(patch, null);
}
String value = strVarbit + ":" + unixNow;
configManager.setRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, key, value);
changed = true;

View File

@@ -0,0 +1,323 @@
/*
* Copyright (c) 2022 LlemonDuck
* 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.Guice;
import com.google.inject.testing.fieldbinder.Bind;
import com.google.inject.testing.fieldbinder.BoundFieldModule;
import java.time.Instant;
import java.util.Collections;
import javax.inject.Inject;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.ItemID;
import net.runelite.api.MenuAction;
import net.runelite.api.ObjectComposition;
import net.runelite.api.Player;
import net.runelite.api.Scene;
import net.runelite.api.Tile;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.config.ConfigManager;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.junit.runner.RunWith;
import static org.mockito.ArgumentMatchers.any;
import org.mockito.Mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class CompostTrackerTest
{
@Inject
private CompostTracker compostTracker;
@Mock
@Bind
private Client client;
@Mock
@Bind
private FarmingWorld farmingWorld;
@Mock
@Bind
private ConfigManager configManager;
@Mock
@Bind
private FarmingRegion farmingRegion;
@Mock
@Bind
private FarmingPatch farmingPatch;
@Mock
@Bind
private GameObject patchObject;
@Mock
@Bind
private Player player;
@Mock
@Bind
private Scene scene;
@Mock
@Bind
private Tile tile;
@Mock
@Bind
private ObjectComposition patchDef;
@Rule
public ErrorCollector collector = new ErrorCollector();
private static final int PATCH_ID = 12345;
private static final int PATCH_VARBIT = 54321;
private static final WorldPoint worldPoint = new WorldPoint(1, 2, 0);
@Before
public void before()
{
Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this);
compostTracker.pendingCompostActions.clear();
when(client.getBaseX()).thenReturn(0);
when(client.getBaseY()).thenReturn(0);
when(client.getPlane()).thenReturn(0);
when(client.getLocalPlayer()).thenReturn(player);
when(player.getWorldLocation()).thenReturn(worldPoint);
when(client.getScene()).thenReturn(scene);
when(client.getObjectDefinition(PATCH_ID)).thenReturn(patchDef);
when(scene.getTiles()).thenReturn(new Tile[][][]{{null, {null, null, tile}}}); // indices match worldPoint
when(tile.getGameObjects()).thenReturn(new GameObject[]{patchObject});
when(farmingWorld.getRegionsForLocation(any())).thenReturn(Collections.singleton(farmingRegion));
when(farmingRegion.getPatches()).thenReturn(new FarmingPatch[]{farmingPatch});
when(farmingPatch.getVarbit()).thenReturn(PATCH_VARBIT);
when(farmingPatch.configKey()).thenReturn("MOCK");
when(patchObject.getId()).thenReturn(PATCH_ID);
when(patchObject.sizeX()).thenReturn(1);
when(patchObject.sizeY()).thenReturn(1);
when(patchDef.getVarbitId()).thenReturn(PATCH_VARBIT);
}
@Test
public void setCompostState_storesNonNullChangesToConfig()
{
compostTracker.setCompostState(farmingPatch, CompostState.COMPOST);
verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.COMPOST);
}
@Test
public void setCompostState_storesNullChangesByClearingConfig()
{
compostTracker.setCompostState(farmingPatch, null);
verify(configManager).unsetRSProfileConfiguration("timetracking", "MOCK.compost");
}
@Test
public void getCompostState_directlyReturnsFromConfig()
{
when(configManager.getRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.class)).thenReturn(
CompostState.SUPERCOMPOST);
assertThat(compostTracker.getCompostState(farmingPatch), is(CompostState.SUPERCOMPOST));
}
@Test
public void determineCompostUsed_returnsAppropriateCompostValues()
{
// invalid
collector.checkThat(
CompostTracker.determineCompostUsed("This is not a farming chat message."),
is((CompostState) null)
);
collector.checkThat(
CompostTracker.determineCompostUsed("Contains word compost but is not examine message."),
is((CompostState) null)
);
// inspect
collector.checkThat(
CompostTracker.determineCompostUsed("This is an allotment. The soil has been treated with supercompost. The patch is empty and weeded."),
is(CompostState.SUPERCOMPOST)
);
// fertile soil on existing patch
collector.checkThat(
CompostTracker.determineCompostUsed("This patch has already been fertilised with ultracompost - the spell can't make it any more fertile."),
is(CompostState.ULTRACOMPOST)
);
// fertile soil on cleared patch
collector.checkThat(
CompostTracker.determineCompostUsed("The herb patch has been treated with supercompost."),
is(CompostState.SUPERCOMPOST)
);
// bucket on cleared patch
collector.checkThat(
CompostTracker.determineCompostUsed("You treat the herb patch with ultracompost."),
is(CompostState.ULTRACOMPOST)
);
collector.checkThat(
CompostTracker.determineCompostUsed("You treat the tree patch with compost."),
is(CompostState.COMPOST)
);
collector.checkThat(
CompostTracker.determineCompostUsed("You treat the fruit tree patch with supercompost."),
is(CompostState.SUPERCOMPOST)
);
}
@Test
public void onMenuOptionClicked_queuesPendingCompostForInspectActions()
{
MenuOptionClicked inspectPatchAction = mock(MenuOptionClicked.class);
when(inspectPatchAction.getMenuAction()).thenReturn(MenuAction.GAME_OBJECT_SECOND_OPTION);
when(inspectPatchAction.getMenuOption()).thenReturn("Inspect");
when(inspectPatchAction.getId()).thenReturn(PATCH_ID);
when(inspectPatchAction.getParam0()).thenReturn(1);
when(inspectPatchAction.getParam1()).thenReturn(2);
compostTracker.onMenuOptionClicked(inspectPatchAction);
CompostTracker.PendingCompost actual = compostTracker.pendingCompostActions.get(farmingPatch);
assertThat(actual.getFarmingPatch(), is(farmingPatch));
assertThat(actual.getPatchLocation(), is(new WorldPoint(1, 2, 0)));
}
@Test
public void onMenuOptionClicked_queuesPendingCompostForCompostActions()
{
Widget widget = mock(Widget.class);
when(client.getSelectedWidget()).thenReturn(widget);
when(widget.getItemId()).thenReturn(ItemID.ULTRACOMPOST);
MenuOptionClicked inspectPatchAction = mock(MenuOptionClicked.class);
when(inspectPatchAction.getMenuAction()).thenReturn(MenuAction.WIDGET_TARGET_ON_GAME_OBJECT);
when(inspectPatchAction.getId()).thenReturn(PATCH_ID);
when(inspectPatchAction.getParam0()).thenReturn(1);
when(inspectPatchAction.getParam1()).thenReturn(2);
compostTracker.onMenuOptionClicked(inspectPatchAction);
CompostTracker.PendingCompost actual = compostTracker.pendingCompostActions.get(farmingPatch);
assertThat(actual.getFarmingPatch(), is(farmingPatch));
assertThat(actual.getPatchLocation(), is(new WorldPoint(1, 2, 0)));
}
@Test
public void onMenuOptionClicked_queuesPendingCompostForFertileSoilSpellActions()
{
Widget widget = mock(Widget.class);
when(client.getSelectedWidget()).thenReturn(widget);
when(widget.getId()).thenReturn(WidgetInfo.SPELL_LUNAR_FERTILE_SOIL.getPackedId());
MenuOptionClicked inspectPatchAction = mock(MenuOptionClicked.class);
when(inspectPatchAction.getMenuAction()).thenReturn(MenuAction.WIDGET_TARGET_ON_GAME_OBJECT);
when(inspectPatchAction.getId()).thenReturn(PATCH_ID);
when(inspectPatchAction.getParam0()).thenReturn(1);
when(inspectPatchAction.getParam1()).thenReturn(2);
compostTracker.onMenuOptionClicked(inspectPatchAction);
CompostTracker.PendingCompost actual = compostTracker.pendingCompostActions.get(farmingPatch);
assertThat(actual.getFarmingPatch(), is(farmingPatch));
assertThat(actual.getPatchLocation(), is(new WorldPoint(1, 2, 0)));
}
@Test
public void onChatMessage_ignoresInvalidTypes()
{
ChatMessage chatEvent = mock(ChatMessage.class);
when(chatEvent.getType()).thenReturn(ChatMessageType.PUBLICCHAT);
compostTracker.onChatMessage(chatEvent);
verifyNoInteractions(client);
verifyNoInteractions(farmingWorld);
}
@Test
public void onChatMessage_handlesInspectMessages()
{
ChatMessage chatEvent = mock(ChatMessage.class);
when(chatEvent.getType()).thenReturn(ChatMessageType.SPAM);
when(chatEvent.getMessage()).thenReturn("This is a tree patch. The soil has been treated with ultracompost. The patch is empty and weeded.");
compostTracker.pendingCompostActions.put(farmingPatch, new CompostTracker.PendingCompost(Instant.MAX, worldPoint, farmingPatch));
compostTracker.onChatMessage(chatEvent);
verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.ULTRACOMPOST);
}
@Test
public void onChatMessage_handlesBucketUseMessages()
{
ChatMessage chatEvent = mock(ChatMessage.class);
when(chatEvent.getType()).thenReturn(ChatMessageType.SPAM);
when(chatEvent.getMessage()).thenReturn("You treat the herb patch with compost.");
compostTracker.pendingCompostActions.put(farmingPatch, new CompostTracker.PendingCompost(Instant.MAX, worldPoint, farmingPatch));
compostTracker.onChatMessage(chatEvent);
verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.COMPOST);
}
@Test
public void onChatMessage_handlesFertileSoilMessages()
{
ChatMessage chatEvent = mock(ChatMessage.class);
when(chatEvent.getType()).thenReturn(ChatMessageType.SPAM);
when(chatEvent.getMessage()).thenReturn("The allotment has been treated with supercompost.");
compostTracker.pendingCompostActions.put(farmingPatch, new CompostTracker.PendingCompost(Instant.MAX, worldPoint, farmingPatch));
compostTracker.onChatMessage(chatEvent);
verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.SUPERCOMPOST);
}
}