plugins: add missing compatible plugins

This commit is contained in:
therealunull
2020-12-14 07:41:02 -05:00
parent 3248f22650
commit aa44329202
504 changed files with 73990 additions and 3845 deletions

View File

@@ -1,136 +0,0 @@
/*
* Copyright (c) 2018, TheLonelyDev <https://github.com/TheLonelyDev>
* Copyright (c) 2018, Jeremy Plsek <https://github.com/jplsek>
* 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.bank;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Keybind;
@ConfigGroup("bank")
public interface BankConfig extends Config
{
@ConfigItem(
keyName = "showGE",
name = "Show Grand Exchange price",
description = "Show grand exchange price total (GE)",
position = 1
)
default boolean showGE()
{
return true;
}
@ConfigItem(
keyName = "showHA",
name = "Show high alchemy price",
description = "Show high alchemy price total (HA)",
position = 2
)
default boolean showHA()
{
return false;
}
@ConfigItem(
keyName = "showExact",
name = "Show exact bank value",
description = "Show exact bank value",
position = 3
)
default boolean showExact()
{
return false;
}
@ConfigItem(
keyName = "rightClickBankInventory",
name = "Disable left click bank inventory",
description = "Configures whether the bank inventory button will bank your inventory on left click",
position = 4
)
default boolean rightClickBankInventory()
{
return false;
}
@ConfigItem(
keyName = "rightClickBankEquip",
name = "Disable left click bank equipment",
description = "Configures whether the bank equipment button will bank your equipment on left click",
position = 5
)
default boolean rightClickBankEquip()
{
return false;
}
@ConfigItem(
keyName = "rightClickBankLoot",
name = "Disable left click bank looting bag",
description = "Configures whether the bank looting bag button will bank your looting bag contents on left click",
position = 6
)
default boolean rightClickBankLoot()
{
return false;
}
@ConfigItem(
keyName = "seedVaultValue",
name = "Show seed vault value",
description = "Adds the total value of all seeds inside the seed vault to the title",
position = 7
)
default boolean seedVaultValue()
{
return true;
}
@ConfigItem(
keyName = "bankPinKeyboard",
name = "Keyboard Bankpin",
description = "Allows using the keyboard keys for bank pin input",
position = 8
)
default boolean bankPinKeyboard()
{
return false;
}
@ConfigItem(
keyName = "searchKeybind",
name = "Search Shortcut",
description = "Keyboard shortcut for initiating a bank search",
position = 9
)
default Keybind searchKeybind()
{
return new Keybind(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK);
}
}

View File

@@ -1,546 +0,0 @@
/*
* Copyright (c) 2018, TheLonelyDev <https://github.com/TheLonelyDev>
* Copyright (c) 2018, Jeremy Plsek <https://github.com/jplsek>
* Copyright (c) 2019, Hydrox6 <ikada@protonmail.ch>
* 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.bank;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.inject.Provides;
import java.awt.event.KeyEvent;
import java.text.ParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemComposition;
import net.runelite.api.ItemContainer;
import net.runelite.api.ItemID;
import net.runelite.api.MenuEntry;
import net.runelite.api.ScriptID;
import net.runelite.api.VarClientStr;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.MenuShouldLeftClick;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.JavaScriptCallback;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.config.Keybind;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.game.ItemManager;
import net.runelite.client.input.KeyListener;
import net.runelite.client.input.KeyManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.util.QuantityFormatter;
@PluginDescriptor(
name = "Bank",
description = "Modifications to the banking interface",
tags = {"grand", "exchange", "high", "alchemy", "prices", "deposit"}
)
@Slf4j
public class BankPlugin extends Plugin
{
private static final String DEPOSIT_WORN = "Deposit worn items";
private static final String DEPOSIT_INVENTORY = "Deposit inventory";
private static final String DEPOSIT_LOOT = "Deposit loot";
private static final String SEED_VAULT_TITLE = "Seed Vault";
private static final String NUMBER_REGEX = "[0-9]+(\\.[0-9]+)?[kmb]?";
private static final Pattern VALUE_SEARCH_PATTERN = Pattern.compile("^(?<mode>ge|ha|alch)?" +
" *(((?<op>[<>=]|>=|<=) *(?<num>" + NUMBER_REGEX + "))|" +
"((?<num1>" + NUMBER_REGEX + ") *- *(?<num2>" + NUMBER_REGEX + ")))$", Pattern.CASE_INSENSITIVE);
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ItemManager itemManager;
@Inject
private BankConfig config;
@Inject
private BankSearch bankSearch;
@Inject
private KeyManager keyManager;
private boolean forceRightClickFlag;
private Multiset<Integer> itemQuantities; // bank item quantities for bank value search
private String searchString;
private final KeyListener searchHotkeyListener = new KeyListener()
{
@Override
public void keyTyped(KeyEvent e)
{
}
@Override
public void keyPressed(KeyEvent e)
{
Keybind keybind = config.searchKeybind();
if (keybind.matches(e))
{
Widget bankContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
if (bankContainer == null || bankContainer.isSelfHidden())
{
return;
}
log.debug("Search hotkey pressed");
bankSearch.initSearch();
e.consume();
}
}
@Override
public void keyReleased(KeyEvent e)
{
}
};
@Provides
BankConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(BankConfig.class);
}
@Override
protected void startUp()
{
keyManager.registerKeyListener(searchHotkeyListener);
}
@Override
protected void shutDown()
{
keyManager.unregisterKeyListener(searchHotkeyListener);
clientThread.invokeLater(() -> bankSearch.reset(false));
forceRightClickFlag = false;
itemQuantities = null;
searchString = null;
}
@Subscribe
public void onMenuShouldLeftClick(MenuShouldLeftClick event)
{
if (!forceRightClickFlag)
{
return;
}
forceRightClickFlag = false;
MenuEntry[] menuEntries = client.getMenuEntries();
for (MenuEntry entry : menuEntries)
{
if ((entry.getOption().equals(DEPOSIT_WORN) && config.rightClickBankEquip())
|| (entry.getOption().equals(DEPOSIT_INVENTORY) && config.rightClickBankInventory())
|| (entry.getOption().equals(DEPOSIT_LOOT) && config.rightClickBankLoot()))
{
event.setForceRightClick(true);
return;
}
}
}
@Subscribe
public void onMenuEntryAdded(MenuEntryAdded event)
{
if ((event.getOption().equals(DEPOSIT_WORN) && config.rightClickBankEquip())
|| (event.getOption().equals(DEPOSIT_INVENTORY) && config.rightClickBankInventory())
|| (event.getOption().equals(DEPOSIT_LOOT) && config.rightClickBankLoot()))
{
forceRightClickFlag = true;
}
}
@Subscribe
public void onScriptCallbackEvent(ScriptCallbackEvent event)
{
int[] intStack = client.getIntStack();
String[] stringStack = client.getStringStack();
int intStackSize = client.getIntStackSize();
int stringStackSize = client.getStringStackSize();
switch (event.getEventName())
{
case "bankSearchFilter":
int itemId = intStack[intStackSize - 1];
String search = stringStack[stringStackSize - 1];
if (valueSearch(itemId, search))
{
// return true
intStack[intStackSize - 2] = 1;
}
break;
case "bankpinButtonSetup":
{
if (!config.bankPinKeyboard())
{
return;
}
final int compId = intStack[intStackSize - 2];
final int buttonId = intStack[intStackSize - 1];
//TODO Implement client.getWidget(compId)
Widget button = null;
Widget buttonRect = button.getChild(0);
final Object[] onOpListener = buttonRect.getOnOpListener();
buttonRect.setOnKeyListener((JavaScriptCallback) e ->
{
int typedChar = e.getTypedKeyChar() - '0';
if (typedChar != buttonId)
{
return;
}
log.debug("Bank pin keypress");
final String input = client.getVar(VarClientStr.CHATBOX_TYPED_TEXT);
clientThread.invokeLater(() ->
{
// reset chatbox input to avoid pin going to chatbox..
client.setVar(VarClientStr.CHATBOX_TYPED_TEXT, input);
client.runScript(ScriptID.CHAT_PROMPT_INIT);
client.runScript(onOpListener);
});
});
break;
}
}
}
@Subscribe
public void onWidgetLoaded(WidgetLoaded event)
{
if (event.getGroupId() != WidgetID.SEED_VAULT_GROUP_ID || !config.seedVaultValue())
{
return;
}
updateSeedVaultTotal();
}
@Subscribe
public void onScriptPostFired(ScriptPostFired event)
{
if (event.getScriptId() == ScriptID.BANKMAIN_BUILD)
{
// Compute bank prices using only the shown items so that we can show bank value during searches
final Widget bankItemContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
final ItemContainer bankContainer = client.getItemContainer(InventoryID.BANK);
final Widget[] children = bankItemContainer.getChildren();
long geTotal = 0, haTotal = 0;
if (children != null)
{
log.debug("Computing bank price of {} items", bankContainer.size());
// The first components are the bank items, followed by tabs etc. There are always 816 components regardless
// of bank size, but we only need to check up to the bank size.
for (int i = 0; i < bankContainer.size(); ++i)
{
Widget child = children[i];
if (child != null && !child.isSelfHidden() && child.getItemId() > -1)
{
final int alchPrice = getHaPrice(child.getItemId());
geTotal += (long) itemManager.getItemPrice(child.getItemId()) * child.getItemQuantity();
haTotal += (long) alchPrice * child.getItemQuantity();
}
}
Widget bankTitle = client.getWidget(WidgetInfo.BANK_TITLE_BAR);
bankTitle.setText(bankTitle.getText() + createValueText(geTotal, haTotal));
}
}
else if (event.getScriptId() == ScriptID.BANKMAIN_SEARCH_REFRESH)
{
// vanilla only lays out the bank every 40 client ticks, so if the search input has changed,
// and the bank wasn't laid out this tick, lay it out early
final String inputText = client.getVar(VarClientStr.INPUT_TEXT);
if (searchString != inputText && client.getGameCycle() % 40 != 0)
{
clientThread.invokeLater(bankSearch::layoutBank);
searchString = inputText;
}
}
}
@Subscribe
public void onItemContainerChanged(ItemContainerChanged event)
{
int containerId = event.getContainerId();
if (containerId == InventoryID.BANK.getId())
{
itemQuantities = null;
}
else if (containerId == InventoryID.SEED_VAULT.getId() && config.seedVaultValue())
{
updateSeedVaultTotal();
}
}
private String createValueText(long gePrice, long haPrice)
{
StringBuilder stringBuilder = new StringBuilder();
if (config.showGE() && gePrice != 0)
{
stringBuilder.append(" (");
if (config.showHA())
{
stringBuilder.append("GE: ");
}
if (config.showExact())
{
stringBuilder.append(QuantityFormatter.formatNumber(gePrice));
}
else
{
stringBuilder.append(QuantityFormatter.quantityToStackSize(gePrice));
}
stringBuilder.append(')');
}
if (config.showHA() && haPrice != 0)
{
stringBuilder.append(" (");
if (config.showGE())
{
stringBuilder.append("HA: ");
}
if (config.showExact())
{
stringBuilder.append(QuantityFormatter.formatNumber(haPrice));
}
else
{
stringBuilder.append(QuantityFormatter.quantityToStackSize(haPrice));
}
stringBuilder.append(')');
}
return stringBuilder.toString();
}
private void updateSeedVaultTotal()
{
final Widget titleContainer = client.getWidget(WidgetInfo.SEED_VAULT_TITLE_CONTAINER);
if (titleContainer == null)
{
return;
}
final Widget title = titleContainer.getChild(1);
if (title == null)
{
return;
}
final ContainerPrices prices = calculate(getSeedVaultItems());
if (prices == null)
{
return;
}
final String titleText = createValueText(prices.getGePrice(), prices.getHighAlchPrice());
title.setText(SEED_VAULT_TITLE + titleText);
}
private Item[] getSeedVaultItems()
{
final ItemContainer itemContainer = client.getItemContainer(InventoryID.SEED_VAULT);
if (itemContainer == null)
{
return null;
}
return itemContainer.getItems();
}
@VisibleForTesting
boolean valueSearch(final int itemId, final String str)
{
final Matcher matcher = VALUE_SEARCH_PATTERN.matcher(str);
if (!matcher.matches())
{
return false;
}
// Count bank items and remember it for determining item quantity
if (itemQuantities == null)
{
itemQuantities = getBankItemSet();
}
final ItemComposition itemComposition = itemManager.getItemComposition(itemId);
final int qty = itemQuantities.count(itemId);
final long gePrice = (long) itemManager.getItemPrice(itemId) * qty;
final long haPrice = (long) itemComposition.getHaPrice() * qty;
long value = Math.max(gePrice, haPrice);
final String mode = matcher.group("mode");
if (mode != null)
{
value = mode.toLowerCase().equals("ge") ? gePrice : haPrice;
}
final String op = matcher.group("op");
if (op != null)
{
long compare;
try
{
compare = QuantityFormatter.parseQuantity(matcher.group("num"));
}
catch (ParseException e)
{
return false;
}
switch (op)
{
case ">":
return value > compare;
case "<":
return value < compare;
case "=":
return value == compare;
case ">=":
return value >= compare;
case "<=":
return value <= compare;
}
}
final String num1 = matcher.group("num1");
final String num2 = matcher.group("num2");
if (num1 != null && num2 != null)
{
long compare1, compare2;
try
{
compare1 = QuantityFormatter.parseQuantity(num1);
compare2 = QuantityFormatter.parseQuantity(num2);
}
catch (ParseException e)
{
return false;
}
return compare1 <= value && compare2 >= value;
}
return false;
}
private Multiset<Integer> getBankItemSet()
{
ItemContainer itemContainer = client.getItemContainer(InventoryID.BANK);
if (itemContainer == null)
{
return HashMultiset.create();
}
Multiset<Integer> set = HashMultiset.create();
for (Item item : itemContainer.getItems())
{
if (item.getId() != ItemID.BANK_FILLER)
{
set.add(item.getId(), item.getQuantity());
}
}
return set;
}
@Nullable
ContainerPrices calculate(@Nullable Item[] items)
{
if (items == null)
{
return null;
}
long ge = 0;
long alch = 0;
for (final Item item : items)
{
final int qty = item.getQuantity();
final int id = item.getId();
if (id <= 0 || qty == 0)
{
continue;
}
alch += (long) getHaPrice(id) * qty;
ge += (long) itemManager.getItemPrice(id) * qty;
}
return new ContainerPrices(ge, alch);
}
private int getHaPrice(int itemId)
{
switch (itemId)
{
case ItemID.COINS_995:
return 1;
case ItemID.PLATINUM_TOKEN:
return 1000;
default:
return itemManager.getItemComposition(itemId).getHaPrice();
}
}
}

View File

@@ -1,119 +0,0 @@
/*
* Copyright (c) 2018, Ron Young <https://github.com/raiyni>
* 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.bank;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.runelite.api.Client;
import net.runelite.api.ScriptID;
import net.runelite.api.VarClientInt;
import net.runelite.api.VarClientStr;
import net.runelite.api.vars.InputType;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.callback.ClientThread;
import org.apache.commons.lang3.ArrayUtils;
@Singleton
public class BankSearch
{
private final Client client;
private final ClientThread clientThread;
@Inject
private BankSearch(
final Client client,
final ClientThread clientThread
)
{
this.client = client;
this.clientThread = clientThread;
}
public void layoutBank()
{
Widget bankContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
if (bankContainer == null || bankContainer.isHidden())
{
return;
}
Object[] scriptArgs = bankContainer.getOnInvTransmitListener();
if (scriptArgs == null)
{
return;
}
client.runScript(scriptArgs);
}
public void initSearch()
{
clientThread.invoke(() ->
{
Widget bankContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
if (bankContainer == null || bankContainer.isHidden())
{
return;
}
Object[] bankBuildArgs = bankContainer.getOnInvTransmitListener();
if (bankBuildArgs == null)
{
return;
}
// the search toggle script requires 1 as its first argument
Object[] searchToggleArgs = ArrayUtils.insert(1, bankBuildArgs, 1);
searchToggleArgs[0] = ScriptID.BANKMAIN_SEARCH_TOGGLE;
// reset search to clear tab tags and also allow us to initiate a new search while searching
reset(true);
client.runScript(searchToggleArgs);
});
}
public void reset(boolean closeChat)
{
clientThread.invoke(() ->
{
// This ensures that any chatbox input (e.g from search) will not remain visible when
// selecting/changing tab
if (closeChat)
{
// this clears the input text and type, and resets the chatbox to allow input
client.runScript(ScriptID.MESSAGE_LAYER_CLOSE, 1, 1);
}
else
{
client.setVar(VarClientInt.INPUT_TYPE, InputType.NONE.getType());
client.setVar(VarClientStr.INPUT_TEXT, "");
}
layoutBank();
});
}
}

View File

@@ -1,598 +0,0 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* Copyright (c) 2018, Ron Young <https://github.com/raiyni>
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.banktags;
import com.google.common.collect.Lists;
import com.google.common.primitives.Shorts;
import com.google.inject.Provides;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemComposition;
import net.runelite.api.ItemContainer;
import net.runelite.api.KeyCode;
import net.runelite.api.MenuAction;
import net.runelite.api.MenuEntry;
import net.runelite.api.ScriptID;
import net.runelite.api.SpriteID;
import net.runelite.api.VarClientStr;
import net.runelite.api.events.DraggingWidgetChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.GrandExchangeSearched;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.ScriptPreFired;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.game.ItemManager;
import net.runelite.client.game.ItemVariationMapping;
import net.runelite.client.game.SpriteManager;
import net.runelite.client.game.chatbox.ChatboxPanelManager;
import net.runelite.client.input.MouseManager;
import net.runelite.client.input.MouseWheelListener;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDependency;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.banktags.tabs.TabInterface;
import static net.runelite.client.plugins.banktags.tabs.TabInterface.FILTERED_CHARS;
import net.runelite.client.plugins.banktags.tabs.TabSprites;
import net.runelite.client.plugins.banktags.tabs.TagTab;
import net.runelite.client.plugins.cluescrolls.ClueScrollPlugin;
import net.runelite.client.util.Text;
@PluginDescriptor(
name = "Bank Tags",
description = "Enable tagging of bank items and searching of bank tags",
tags = {"searching", "tagging"}
)
@PluginDependency(ClueScrollPlugin.class)
public class BankTagsPlugin extends Plugin implements MouseWheelListener
{
public static final String CONFIG_GROUP = "banktags";
public static final String TAG_SEARCH = "tag:";
private static final String EDIT_TAGS_MENU_OPTION = "Edit-tags";
public static final String ICON_SEARCH = "icon_";
public static final String TAG_TABS_CONFIG = "tagtabs";
public static final String VAR_TAG_SUFFIX = "*";
private static final int ITEMS_PER_ROW = 8;
private static final int ITEM_VERTICAL_SPACING = 36;
private static final int ITEM_HORIZONTAL_SPACING = 48;
private static final int ITEM_ROW_START = 51;
private static final int MAX_RESULT_COUNT = 250;
private static final String SEARCH_BANK_INPUT_TEXT =
"Show items whose names or tags contain the following text:<br>" +
"(To show only tagged items, start your search with 'tag:')";
private static final String SEARCH_BANK_INPUT_TEXT_FOUND =
"Show items whose names or tags contain the following text: (%d found)<br>" +
"(To show only tagged items, start your search with 'tag:')";
@Inject
private ItemManager itemManager;
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ChatboxPanelManager chatboxPanelManager;
@Inject
private MouseManager mouseManager;
@Inject
private BankTagsConfig config;
@Inject
private TagManager tagManager;
@Inject
private TabInterface tabInterface;
@Inject
private SpriteManager spriteManager;
@Inject
private ConfigManager configManager;
@Provides
BankTagsConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(BankTagsConfig.class);
}
@Override
public void resetConfiguration()
{
List<String> extraKeys = Lists.newArrayList(
CONFIG_GROUP + "." + TagManager.ITEM_KEY_PREFIX,
CONFIG_GROUP + "." + ICON_SEARCH,
CONFIG_GROUP + "." + TAG_TABS_CONFIG
);
for (String prefix : extraKeys)
{
List<String> keys = configManager.getConfigurationKeys(prefix);
for (String key : keys)
{
String[] str = key.split("\\.", 2);
if (str.length == 2)
{
configManager.unsetConfiguration(str[0], str[1]);
}
}
}
clientThread.invokeLater(() ->
{
tabInterface.destroy();
tabInterface.init();
});
}
@Override
public void startUp()
{
cleanConfig();
mouseManager.registerMouseWheelListener(this);
clientThread.invokeLater(tabInterface::init);
spriteManager.addSpriteOverrides(TabSprites.values());
}
@Deprecated
private void cleanConfig()
{
removeInvalidTags("tagtabs");
List<String> tags = configManager.getConfigurationKeys(CONFIG_GROUP + ".item_");
tags.forEach(s ->
{
String[] split = s.split("\\.", 2);
removeInvalidTags(split[1]);
});
List<String> icons = configManager.getConfigurationKeys(CONFIG_GROUP + ".icon_");
icons.forEach(s ->
{
String[] split = s.split("\\.", 2);
String replaced = split[1].replaceAll("[<>/]", "");
if (!split[1].equals(replaced))
{
String value = configManager.getConfiguration(CONFIG_GROUP, split[1]);
configManager.unsetConfiguration(CONFIG_GROUP, split[1]);
if (replaced.length() > "icon_".length())
{
configManager.setConfiguration(CONFIG_GROUP, replaced, value);
}
}
});
}
@Deprecated
private void removeInvalidTags(final String key)
{
final String value = configManager.getConfiguration(CONFIG_GROUP, key);
if (value == null)
{
return;
}
String replaced = value.replaceAll("[<>:/]", "");
if (!value.equals(replaced))
{
replaced = Text.toCSV(Text.fromCSV(replaced));
if (replaced.isEmpty())
{
configManager.unsetConfiguration(CONFIG_GROUP, key);
}
else
{
configManager.setConfiguration(CONFIG_GROUP, key, replaced);
}
}
}
@Override
public void shutDown()
{
mouseManager.unregisterMouseWheelListener(this);
clientThread.invokeLater(tabInterface::destroy);
spriteManager.removeSpriteOverrides(TabSprites.values());
}
@Subscribe
public void onGrandExchangeSearched(GrandExchangeSearched event)
{
final String input = client.getVar(VarClientStr.INPUT_TEXT);
if (!input.startsWith(TAG_SEARCH))
{
return;
}
event.consume();
final String tag = input.substring(TAG_SEARCH.length()).trim();
final Set<Integer> ids = tagManager.getItemsForTag(tag)
.stream()
.mapToInt(Math::abs)
.mapToObj(ItemVariationMapping::getVariations)
.flatMap(Collection::stream)
.distinct()
.filter(i -> itemManager.getItemComposition(i).isTradeable())
.limit(MAX_RESULT_COUNT)
.collect(Collectors.toCollection(TreeSet::new));
client.setGeSearchResultIndex(0);
client.setGeSearchResultCount(ids.size());
client.setGeSearchResultIds(Shorts.toArray(ids));
}
@Subscribe
public void onScriptCallbackEvent(ScriptCallbackEvent event)
{
String eventName = event.getEventName();
int[] intStack = client.getIntStack();
String[] stringStack = client.getStringStack();
int intStackSize = client.getIntStackSize();
int stringStackSize = client.getStringStackSize();
tabInterface.handleScriptEvent(event);
switch (eventName)
{
case "setSearchBankInputText":
stringStack[stringStackSize - 1] = SEARCH_BANK_INPUT_TEXT;
break;
case "setSearchBankInputTextFound":
{
int matches = intStack[intStackSize - 1];
stringStack[stringStackSize - 1] = String.format(SEARCH_BANK_INPUT_TEXT_FOUND, matches);
break;
}
case "bankSearchFilter":
final int itemId = intStack[intStackSize - 1];
final String searchfilter = stringStack[stringStackSize - 1];
// This event only fires when the bank is in search mode. It will fire even if there is no search
// input. We prevent having a tag tab open while also performing a normal search, so if a tag tab
// is active here it must mean we have placed the bank into search mode. See onScriptPostFired().
TagTab activeTab = tabInterface.getActiveTab();
String search = activeTab != null ? TAG_SEARCH + activeTab.getTag() : searchfilter;
if (search.isEmpty())
{
return;
}
boolean tagSearch = search.startsWith(TAG_SEARCH);
if (tagSearch)
{
search = search.substring(TAG_SEARCH.length()).trim();
}
if (tagManager.findTag(itemId, search))
{
// return true
intStack[intStackSize - 2] = 1;
}
else if (tagSearch)
{
// if the item isn't tagged we return false to prevent the item matching if the item name happens
// to contain the tag name.
intStack[intStackSize - 2] = 0;
}
break;
case "getSearchingTagTab":
intStack[intStackSize - 1] = tabInterface.isActive() ? 1 : 0;
break;
}
}
@Subscribe
public void onMenuEntryAdded(MenuEntryAdded event)
{
MenuEntry[] entries = client.getMenuEntries();
if (event.getActionParam1() == WidgetInfo.BANK_ITEM_CONTAINER.getId()
&& event.getOption().equals("Examine"))
{
Widget container = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
Widget item = container.getChild(event.getActionParam());
int itemID = item.getItemId();
String text = EDIT_TAGS_MENU_OPTION;
int tagCount = tagManager.getTags(itemID, false).size() + tagManager.getTags(itemID, true).size();
if (tagCount > 0)
{
text += " (" + tagCount + ")";
}
MenuEntry editTags = new MenuEntry();
editTags.setActionParam(event.getActionParam());
editTags.setParam1(event.getActionParam1());
editTags.setTarget(event.getTarget());
editTags.setOption(text);
editTags.setType(MenuAction.RUNELITE.getId());
editTags.setIdentifier(event.getIdentifier());
entries = Arrays.copyOf(entries, entries.length + 1);
entries[entries.length - 1] = editTags;
client.setMenuEntries(entries);
}
tabInterface.handleAdd(event);
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked event)
{
if (event.getWidgetId() == WidgetInfo.BANK_ITEM_CONTAINER.getId()
&& event.getMenuAction() == MenuAction.RUNELITE
&& event.getMenuOption().startsWith(EDIT_TAGS_MENU_OPTION))
{
event.consume();
int inventoryIndex = event.getActionParam();
ItemContainer bankContainer = client.getItemContainer(InventoryID.BANK);
if (bankContainer == null)
{
return;
}
Item[] items = bankContainer.getItems();
if (inventoryIndex < 0 || inventoryIndex >= items.length)
{
return;
}
Item item = bankContainer.getItems()[inventoryIndex];
if (item == null)
{
return;
}
int itemId = item.getId();
ItemComposition itemComposition = itemManager.getItemComposition(itemId);
String name = itemComposition.getName();
// Get both tags and vartags and append * to end of vartags name
Collection<String> tags = tagManager.getTags(itemId, false);
tagManager.getTags(itemId, true).stream()
.map(i -> i + "*")
.forEach(tags::add);
String initialValue = Text.toCSV(tags);
chatboxPanelManager.openTextInput(name + " tags:<br>(append " + VAR_TAG_SUFFIX + " for variation tag)")
.addCharValidator(FILTERED_CHARS)
.value(initialValue)
.onDone((Consumer<String>) (newValue) ->
clientThread.invoke(() ->
{
// Split inputted tags to vartags (ending with *) and regular tags
final Collection<String> newTags = new ArrayList<>(Text.fromCSV(newValue.toLowerCase()));
final Collection<String> newVarTags = new ArrayList<>(newTags).stream().filter(s -> s.endsWith(VAR_TAG_SUFFIX)).map(s ->
{
newTags.remove(s);
return s.substring(0, s.length() - VAR_TAG_SUFFIX.length());
}).collect(Collectors.toList());
// And save them
tagManager.setTagString(itemId, Text.toCSV(newTags), false);
tagManager.setTagString(itemId, Text.toCSV(newVarTags), true);
// Check both previous and current tags in case the tag got removed in new tags or in case
// the tag got added in new tags
tabInterface.updateTabIfActive(Text.fromCSV(initialValue.toLowerCase().replaceAll(Pattern.quote(VAR_TAG_SUFFIX), "")));
tabInterface.updateTabIfActive(Text.fromCSV(newValue.toLowerCase().replaceAll(Pattern.quote(VAR_TAG_SUFFIX), "")));
}))
.build();
}
else
{
tabInterface.handleClick(event);
}
}
@Subscribe
public void onConfigChanged(ConfigChanged configChanged)
{
if (configChanged.getGroup().equals(CONFIG_GROUP) && configChanged.getKey().equals("useTabs"))
{
if (config.tabs())
{
clientThread.invokeLater(tabInterface::init);
}
else
{
clientThread.invokeLater(tabInterface::destroy);
}
}
}
@Subscribe
public void onScriptPreFired(ScriptPreFired event)
{
int scriptId = event.getScriptId();
if (scriptId == ScriptID.BANKMAIN_FINISHBUILDING)
{
// Since we apply tag tab search filters even when the bank is not in search mode,
// bankkmain_build will reset the bank title to "The Bank of Gielinor". So apply our
// own title.
TagTab activeTab = tabInterface.getActiveTab();
if (tabInterface.isTagTabActive())
{
// Tag tab tab has its own title since it isn't a real tag
Widget bankTitle = client.getWidget(WidgetInfo.BANK_TITLE_BAR);
bankTitle.setText("Tag tab tab");
}
else if (activeTab != null)
{
Widget bankTitle = client.getWidget(WidgetInfo.BANK_TITLE_BAR);
bankTitle.setText("Tag tab <col=ff0000>" + activeTab.getTag() + "</col>");
}
}
else if (scriptId == ScriptID.BANKMAIN_SEARCH_TOGGLE)
{
tabInterface.handleSearch();
}
}
@Subscribe
public void onScriptPostFired(ScriptPostFired event)
{
if (event.getScriptId() == ScriptID.BANKMAIN_SEARCHING)
{
// The return value of bankmain_searching is on the stack. If we have a tag tab active
// make it return true to put the bank in a searching state.
if (tabInterface.getActiveTab() != null || tabInterface.isTagTabActive())
{
client.getIntStack()[client.getIntStackSize() - 1] = 1; // true
}
return;
}
if (event.getScriptId() != ScriptID.BANKMAIN_BUILD || !config.removeSeparators())
{
return;
}
if (!tabInterface.isActive())
{
return;
}
Widget itemContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
if (itemContainer == null)
{
return;
}
int items = 0;
Widget[] containerChildren = itemContainer.getDynamicChildren();
// sort the child array as the items are not in the displayed order
Arrays.sort(containerChildren, Comparator.comparing(Widget::getOriginalY)
.thenComparing(Widget::getOriginalX));
for (Widget child : containerChildren)
{
if (child.getItemId() != -1 && !child.isHidden())
{
// calculate correct item position as if this was a normal tab
int adjYOffset = (items / ITEMS_PER_ROW) * ITEM_VERTICAL_SPACING;
int adjXOffset = (items % ITEMS_PER_ROW) * ITEM_HORIZONTAL_SPACING + ITEM_ROW_START;
if (child.getOriginalY() != adjYOffset)
{
child.setOriginalY(adjYOffset);
child.revalidate();
}
if (child.getOriginalX() != adjXOffset)
{
child.setOriginalX(adjXOffset);
child.revalidate();
}
items++;
}
// separator line or tab text
if (child.getSpriteId() == SpriteID.RESIZEABLE_MODE_SIDE_PANEL_BACKGROUND
|| child.getText().contains("Tab"))
{
child.setHidden(true);
}
}
final Widget bankItemContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
int itemContainerHeight = bankItemContainer.getHeight();
// add a second row of height here to allow users to scroll down when the last row is partially visible
int adjustedScrollHeight = (items / ITEMS_PER_ROW) * ITEM_VERTICAL_SPACING + ITEM_VERTICAL_SPACING;
itemContainer.setScrollHeight(Math.max(adjustedScrollHeight, itemContainerHeight));
final int itemContainerScroll = bankItemContainer.getScrollY();
clientThread.invokeLater(() ->
client.runScript(ScriptID.UPDATE_SCROLLBAR,
WidgetInfo.BANK_SCROLLBAR.getId(),
WidgetInfo.BANK_ITEM_CONTAINER.getId(),
itemContainerScroll));
}
@Subscribe
public void onGameTick(GameTick event)
{
tabInterface.update();
}
@Subscribe
public void onDraggingWidgetChanged(DraggingWidgetChanged event)
{
final boolean shiftPressed = client.isKeyPressed(KeyCode.KC_SHIFT);
tabInterface.handleDrag(event.isDraggingWidget(), shiftPressed);
}
@Subscribe
public void onWidgetLoaded(WidgetLoaded event)
{
if (event.getGroupId() == WidgetID.BANK_GROUP_ID)
{
tabInterface.init();
}
}
@Override
public MouseWheelEvent mouseWheelMoved(MouseWheelEvent event)
{
tabInterface.handleWheel(event);
return event;
}
}

View File

@@ -1,231 +0,0 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2018, Ron Young <https://github.com/raiyni>
* 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.banktags;
import com.google.common.base.Strings;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.runelite.api.ItemID;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.game.ItemManager;
import net.runelite.client.game.ItemVariationMapping;
import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
import net.runelite.client.plugins.cluescrolls.ClueScrollService;
import net.runelite.client.plugins.cluescrolls.clues.ClueScroll;
import net.runelite.client.plugins.cluescrolls.clues.CoordinateClue;
import net.runelite.client.plugins.cluescrolls.clues.EmoteClue;
import net.runelite.client.plugins.cluescrolls.clues.FairyRingClue;
import net.runelite.client.plugins.cluescrolls.clues.HotColdClue;
import net.runelite.client.plugins.cluescrolls.clues.MapClue;
import net.runelite.client.plugins.cluescrolls.clues.item.ItemRequirement;
import net.runelite.client.util.Text;
@Singleton
public class TagManager
{
static final String ITEM_KEY_PREFIX = "item_";
private final ConfigManager configManager;
private final ItemManager itemManager;
private final ClueScrollService clueScrollService;
@Inject
private TagManager(
final ItemManager itemManager,
final ConfigManager configManager,
final ClueScrollService clueScrollService)
{
this.itemManager = itemManager;
this.configManager = configManager;
this.clueScrollService = clueScrollService;
}
String getTagString(int itemId, boolean variation)
{
itemId = getItemId(itemId, variation);
String config = configManager.getConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
if (config == null)
{
return "";
}
return config;
}
Collection<String> getTags(int itemId, boolean variation)
{
return new LinkedHashSet<>(Text.fromCSV(getTagString(itemId, variation).toLowerCase()));
}
void setTagString(int itemId, String tags, boolean variation)
{
itemId = getItemId(itemId, variation);
if (Strings.isNullOrEmpty(tags))
{
configManager.unsetConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
}
else
{
configManager.setConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId, tags);
}
}
public void addTags(int itemId, final Collection<String> t, boolean variation)
{
final Collection<String> tags = getTags(itemId, variation);
if (tags.addAll(t))
{
setTags(itemId, tags, variation);
}
}
public void addTag(int itemId, String tag, boolean variation)
{
final Collection<String> tags = getTags(itemId, variation);
if (tags.add(Text.standardize(tag)))
{
setTags(itemId, tags, variation);
}
}
private void setTags(int itemId, Collection<String> tags, boolean variation)
{
setTagString(itemId, Text.toCSV(tags), variation);
}
boolean findTag(int itemId, String search)
{
if (search.equals("clue") && testClue(itemId))
{
return true;
}
Collection<String> tags = getTags(itemId, false);
tags.addAll(getTags(itemId, true));
return tags.stream().anyMatch(tag -> tag.startsWith(Text.standardize(search)));
}
public List<Integer> getItemsForTag(String tag)
{
final String prefix = CONFIG_GROUP + "." + ITEM_KEY_PREFIX;
return configManager.getConfigurationKeys(prefix).stream()
.map(item -> Integer.parseInt(item.replace(prefix, "")))
.filter(item -> getTags(item, false).contains(tag) || getTags(item, true).contains(tag))
.collect(Collectors.toList());
}
public void removeTag(String tag)
{
final String prefix = CONFIG_GROUP + "." + ITEM_KEY_PREFIX;
configManager.getConfigurationKeys(prefix).forEach(item ->
{
int id = Integer.parseInt(item.replace(prefix, ""));
removeTag(id, tag);
});
}
public void removeTag(int itemId, String tag)
{
Collection<String> tags = getTags(itemId, false);
if (tags.remove(Text.standardize(tag)))
{
setTags(itemId, tags, false);
}
tags = getTags(itemId, true);
if (tags.remove(Text.standardize(tag)))
{
setTags(itemId, tags, true);
}
}
public void renameTag(String oldTag, String newTag)
{
List<Integer> items = getItemsForTag(Text.standardize(oldTag));
items.forEach(id ->
{
Collection<String> tags = getTags(id, id < 0);
tags.remove(Text.standardize(oldTag));
tags.add(Text.standardize(newTag));
setTags(id, tags, id < 0);
});
}
private int getItemId(int itemId, boolean variation)
{
itemId = Math.abs(itemId);
itemId = itemManager.canonicalize(itemId);
if (variation)
{
itemId = ItemVariationMapping.map(itemId) * -1;
}
return itemId;
}
private boolean testClue(int itemId)
{
ClueScroll c = clueScrollService.getClue();
if (c == null)
{
return false;
}
if (c instanceof EmoteClue)
{
EmoteClue emote = (EmoteClue) c;
for (ItemRequirement ir : emote.getItemRequirements())
{
if (ir.fulfilledBy(itemId))
{
return true;
}
}
}
else if (c instanceof CoordinateClue || c instanceof HotColdClue || c instanceof FairyRingClue)
{
return itemId == ItemID.SPADE;
}
else if (c instanceof MapClue)
{
MapClue mapClue = (MapClue) c;
return mapClue.getObjectId() == -1 && itemId == ItemID.SPADE;
}
return false;
}
}

View File

@@ -1,167 +0,0 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2018, Ron Young <https://github.com/raiyni>
* 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.banktags.tabs;
import com.google.common.base.MoreObjects;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.Getter;
import net.runelite.api.ItemID;
import net.runelite.client.config.ConfigManager;
import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
import static net.runelite.client.plugins.banktags.BankTagsPlugin.ICON_SEARCH;
import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_TABS_CONFIG;
import net.runelite.client.util.Text;
import org.apache.commons.lang3.math.NumberUtils;
@Singleton
class TabManager
{
@Getter
private final List<TagTab> tabs = new ArrayList<>();
private final ConfigManager configManager;
@Inject
private TabManager(ConfigManager configManager)
{
this.configManager = configManager;
}
void add(TagTab tagTab)
{
if (!contains(tagTab.getTag()))
{
tabs.add(tagTab);
}
}
void clear()
{
tabs.forEach(t -> t.setHidden(true));
tabs.clear();
}
TagTab find(String tag)
{
Optional<TagTab> first = tabs.stream().filter(t -> t.getTag().equals(Text.standardize(tag))).findAny();
return first.orElse(null);
}
List<String> getAllTabs()
{
return Text.fromCSV(MoreObjects.firstNonNull(configManager.getConfiguration(CONFIG_GROUP, TAG_TABS_CONFIG), ""));
}
TagTab load(String tag)
{
TagTab tagTab = find(tag);
if (tagTab == null)
{
tag = Text.standardize(tag);
String item = configManager.getConfiguration(CONFIG_GROUP, ICON_SEARCH + tag);
int itemid = NumberUtils.toInt(item, ItemID.SPADE);
tagTab = new TagTab(itemid, tag);
}
return tagTab;
}
void swap(String tagToMove, String tagDestination)
{
tagToMove = Text.standardize(tagToMove);
tagDestination = Text.standardize(tagDestination);
if (contains(tagToMove) && contains(tagDestination))
{
Collections.swap(tabs, indexOf(tagToMove), indexOf(tagDestination));
}
}
void insert(String tagToMove, String tagDestination)
{
tagToMove = Text.standardize(tagToMove);
tagDestination = Text.standardize(tagDestination);
if (contains(tagToMove) && contains(tagDestination))
{
tabs.add(indexOf(tagDestination), tabs.remove(indexOf(tagToMove)));
}
}
void remove(String tag)
{
TagTab tagTab = find(tag);
if (tagTab != null)
{
tagTab.setHidden(true);
tabs.remove(tagTab);
removeIcon(tag);
}
}
void save()
{
String tags = Text.toCSV(tabs.stream().map(TagTab::getTag).collect(Collectors.toList()));
configManager.setConfiguration(CONFIG_GROUP, TAG_TABS_CONFIG, tags);
}
void removeIcon(final String tag)
{
configManager.unsetConfiguration(CONFIG_GROUP, ICON_SEARCH + Text.standardize(tag));
}
void setIcon(final String tag, final String icon)
{
configManager.setConfiguration(CONFIG_GROUP, ICON_SEARCH + Text.standardize(tag), icon);
}
int size()
{
return tabs.size();
}
private boolean contains(String tag)
{
return tabs.stream().anyMatch(t -> t.getTag().equals(tag));
}
private int indexOf(TagTab tagTab)
{
return tabs.indexOf(tagTab);
}
private int indexOf(String tag)
{
return indexOf(find(tag));
}
}

View File

@@ -1,183 +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.camera;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Range;
@ConfigGroup("zoom") // using the old plugin's group name
public interface CameraConfig extends Config
{
int OUTER_LIMIT_MIN = -400;
int OUTER_LIMIT_MAX = 400;
/**
* The largest (most zoomed in) value that can be used without the client crashing.
*
* Larger values trigger an overflow in the engine's fov to scale code.
*/
int INNER_ZOOM_LIMIT = 1004;
@ConfigItem(
keyName = "inner",
name = "Expand inner zoom limit",
description = "Configures whether or not the inner zoom limit is reduced",
position = 1
)
default boolean innerLimit()
{
return false;
}
@Range(
min = OUTER_LIMIT_MIN,
max = OUTER_LIMIT_MAX
)
@ConfigItem(
keyName = "outerLimit",
name = "Expand outer zoom limit",
description = "Configures how much the outer zoom limit is adjusted",
position = 2
)
default int outerLimit()
{
return 0;
}
@ConfigItem(
keyName = "relaxCameraPitch",
name = "Vertical camera",
description = "Relax the camera's upper pitch limit",
position = 3
)
default boolean relaxCameraPitch()
{
return false;
}
@ConfigItem(
keyName = "controlFunction",
name = "Control Function",
description = "Configures the zoom function when control is pressed",
position = 4
)
default ControlFunction controlFunction()
{
return ControlFunction.NONE;
}
@ConfigItem(
keyName = "ctrlZoomValue",
name = "Reset zoom position",
description = "Position of zoom when it is reset",
position = 5
)
@Range(
min = OUTER_LIMIT_MIN,
max = INNER_ZOOM_LIMIT
)
default int ctrlZoomValue()
{
return 512;
}
@ConfigItem(
keyName = "zoomIncrement",
name = "Zoom Speed",
description = "Speed of zoom",
position = 6
)
default int zoomIncrement()
{
return 25;
}
@ConfigItem(
keyName = "rightClickMovesCamera",
name = "Right click moves camera",
description = "Remaps right click to middle mouse click if there are no menu options",
position = 7
)
default boolean rightClickMovesCamera()
{
return false;
}
@ConfigItem(
keyName = "ignoreExamine",
name = "Ignore Examine",
description = "Ignore the Examine menu entry",
position = 8
)
default boolean ignoreExamine()
{
return false;
}
@ConfigItem(
keyName = "middleClickMenu",
name = "Middle-button opens menu",
description = "Middle-mouse button always opens the menu",
position = 9
)
default boolean middleClickMenu()
{
return false;
}
@ConfigItem(
keyName = "compassLook",
name = "Compass options",
description = "Adds Look South, East, and West options to the compass",
position = 10
)
default boolean compassLook()
{
return true;
}
@ConfigItem(
keyName = "invertYaw",
name = "Invert Yaw",
description = "Makes moving the camera horizontally with the mouse backwards",
position = 11
)
default boolean invertYaw()
{
return false;
}
@ConfigItem(
keyName = "invertPitch",
name = "Invert Pitch",
description = "Makes moving the camera vertically with the mouse backwards",
position = 12
)
default boolean invertPitch()
{
return false;
}
}

View File

@@ -1,529 +0,0 @@
/*
* Copyright (c) 2018 Abex
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* Copyright (c) 2019, Wynadorn <https://github.com/Wynadorn>
* 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.camera;
import com.google.common.primitives.Ints;
import com.google.inject.Provides;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import javax.inject.Inject;
import javax.swing.SwingUtilities;
import net.runelite.api.Client;
import net.runelite.api.MenuAction;
import net.runelite.api.MenuEntry;
import net.runelite.api.ScriptID;
import net.runelite.api.SettingID;
import net.runelite.api.VarClientInt;
import net.runelite.api.VarPlayer;
import net.runelite.api.events.BeforeRender;
import net.runelite.api.events.ClientTick;
import net.runelite.api.events.FocusChanged;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.api.events.ScriptPreFired;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.JavaScriptCallback;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.input.KeyListener;
import net.runelite.client.input.KeyManager;
import net.runelite.client.input.MouseListener;
import net.runelite.client.input.MouseManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.tooltip.Tooltip;
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
@PluginDescriptor(
name = "Camera",
description = "Expands zoom limit, provides vertical camera, and remaps mouse input keys",
tags = {"zoom", "limit", "vertical", "click", "mouse"},
enabledByDefault = false
)
public class CameraPlugin extends Plugin implements KeyListener, MouseListener
{
private static final int DEFAULT_ZOOM_INCREMENT = 25;
private static final int DEFAULT_OUTER_ZOOM_LIMIT = 128;
static final int DEFAULT_INNER_ZOOM_LIMIT = 896;
private static final String LOOK_NORTH = "Look North";
private static final String LOOK_SOUTH = "Look South";
private static final String LOOK_EAST = "Look East";
private static final String LOOK_WEST = "Look West";
private boolean controlDown;
// flags used to store the mousedown states
private boolean rightClick;
private boolean middleClick;
/**
* Whether or not the current menu has any non-ignored menu entries
*/
private boolean menuHasEntries;
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private CameraConfig config;
@Inject
private KeyManager keyManager;
@Inject
private MouseManager mouseManager;
@Inject
private TooltipManager tooltipManager;
private Tooltip sliderTooltip;
@Provides
CameraConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(CameraConfig.class);
}
@Override
protected void startUp()
{
rightClick = false;
middleClick = false;
menuHasEntries = false;
copyConfigs();
keyManager.registerKeyListener(this);
mouseManager.registerMouseListener(this);
clientThread.invoke(() ->
{
Widget sideSlider = client.getWidget(WidgetInfo.SETTINGS_SIDE_CAMERA_ZOOM_SLIDER_TRACK);
if (sideSlider != null)
{
addZoomTooltip(sideSlider);
}
Widget settingsInit = client.getWidget(WidgetInfo.SETTINGS_INIT);
if (settingsInit != null)
{
//TODO: Implement client.createScriptEvent
//client.createScriptEvent(settingsInit.getOnLoadListener())
//.setSource(settingsInit)
//.run();
}
});
}
@Override
protected void shutDown()
{
client.setCameraPitchRelaxerEnabled(false);
client.setInvertYaw(false);
client.setInvertPitch(false);
keyManager.unregisterKeyListener(this);
mouseManager.unregisterMouseListener(this);
controlDown = false;
clientThread.invoke(() ->
{
Widget sideSlider = client.getWidget(WidgetInfo.SETTINGS_SIDE_CAMERA_ZOOM_SLIDER_TRACK);
if (sideSlider != null)
{
sideSlider.setOnMouseRepeatListener((Object[]) null);
}
Widget settingsInit = client.getWidget(WidgetInfo.SETTINGS_INIT);
if (settingsInit != null)
{
//TODO: Implement client.createScriptEvent
//client.createScriptEvent(settingsInit.getOnLoadListener())
//.setSource(settingsInit)
//.run();
}
});
}
void copyConfigs()
{
client.setCameraPitchRelaxerEnabled(config.relaxCameraPitch());
client.setInvertYaw(config.invertYaw());
client.setInvertPitch(config.invertPitch());
}
@Subscribe
public void onMenuEntryAdded(MenuEntryAdded menuEntryAdded)
{
if (menuEntryAdded.getType() == MenuAction.CC_OP.getId() && menuEntryAdded.getOption().equals(LOOK_NORTH) && config.compassLook())
{
MenuEntry[] menuEntries = client.getMenuEntries();
int len = menuEntries.length;
MenuEntry north = menuEntries[len - 1];
menuEntries = Arrays.copyOf(menuEntries, len + 3);
// The handling for these entries is done in ToplevelCompassOp.rs2asm
menuEntries[--len] = createCameraLookEntry(menuEntryAdded, 4, LOOK_WEST);
menuEntries[++len] = createCameraLookEntry(menuEntryAdded, 3, LOOK_EAST);
menuEntries[++len] = createCameraLookEntry(menuEntryAdded, 2, LOOK_SOUTH);
menuEntries[++len] = north;
client.setMenuEntries(menuEntries);
}
}
private MenuEntry createCameraLookEntry(MenuEntryAdded lookNorth, int identifier, String option)
{
MenuEntry m = new MenuEntry();
m.setOption(option);
m.setTarget(lookNorth.getTarget());
m.setIdentifier(identifier);
m.setType(MenuAction.CC_OP.getId());
m.setParam0(lookNorth.getActionParam0());
m.setParam1(lookNorth.getActionParam1());
return m;
}
@Subscribe
public void onScriptCallbackEvent(ScriptCallbackEvent event)
{
if (client.getIndexScripts().isOverlayOutdated())
{
// if any cache overlay fails to load then assume at least one of the zoom scripts is outdated
// and prevent zoom extending entirely.
return;
}
int[] intStack = client.getIntStack();
int intStackSize = client.getIntStackSize();
if (!controlDown && "scrollWheelZoom".equals(event.getEventName()) && config.controlFunction() == ControlFunction.CONTROL_TO_ZOOM)
{
intStack[intStackSize - 1] = 1;
}
if ("innerZoomLimit".equals(event.getEventName()) && config.innerLimit())
{
intStack[intStackSize - 1] = CameraConfig.INNER_ZOOM_LIMIT;
return;
}
if ("outerZoomLimit".equals(event.getEventName()))
{
int outerLimit = Ints.constrainToRange(config.outerLimit(), CameraConfig.OUTER_LIMIT_MIN, CameraConfig.OUTER_LIMIT_MAX);
int outerZoomLimit = DEFAULT_OUTER_ZOOM_LIMIT - outerLimit;
intStack[intStackSize - 1] = outerZoomLimit;
return;
}
if ("scrollWheelZoomIncrement".equals(event.getEventName()) && config.zoomIncrement() != DEFAULT_ZOOM_INCREMENT)
{
intStack[intStackSize - 1] = config.zoomIncrement();
return;
}
if (config.innerLimit())
{
// This lets the options panel's slider have an exponential rate
final double exponent = 2.d;
switch (event.getEventName())
{
case "zoomLinToExp":
{
double range = intStack[intStackSize - 1];
double value = intStack[intStackSize - 2];
value = Math.pow(value / range, exponent) * range;
intStack[intStackSize - 2] = (int) value;
break;
}
case "zoomExpToLin":
{
double range = intStack[intStackSize - 1];
double value = intStack[intStackSize - 2];
value = Math.pow(value / range, 1.d / exponent) * range;
intStack[intStackSize - 2] = (int) value;
break;
}
}
}
}
@Subscribe
public void onFocusChanged(FocusChanged event)
{
if (!event.isFocused())
{
controlDown = false;
}
}
@Subscribe
public void onConfigChanged(ConfigChanged ev)
{
copyConfigs();
}
@Override
public void keyTyped(KeyEvent e)
{
}
@Override
public void keyPressed(KeyEvent e)
{
if (e.getKeyCode() == KeyEvent.VK_CONTROL)
{
controlDown = true;
}
}
@Override
public void keyReleased(KeyEvent e)
{
if (e.getKeyCode() == KeyEvent.VK_CONTROL)
{
controlDown = false;
if (config.controlFunction() == ControlFunction.CONTROL_TO_RESET)
{
final int zoomValue = Ints.constrainToRange(config.ctrlZoomValue(), CameraConfig.OUTER_LIMIT_MIN, CameraConfig.INNER_ZOOM_LIMIT);
clientThread.invokeLater(() -> client.runScript(ScriptID.CAMERA_DO_ZOOM, zoomValue, zoomValue));
}
}
}
/**
* Checks if the menu has any non-ignored entries
*/
private boolean hasMenuEntries(MenuEntry[] menuEntries)
{
for (MenuEntry menuEntry : menuEntries)
{
MenuAction action = MenuAction.of(menuEntry.getType());
switch (action)
{
case CANCEL:
case WALK:
break;
case EXAMINE_OBJECT:
case EXAMINE_NPC:
case EXAMINE_ITEM_GROUND:
case EXAMINE_ITEM:
case CC_OP_LOW_PRIORITY:
if (config.ignoreExamine())
{
break;
}
default:
return true;
}
}
return false;
}
/**
* Checks if the menu has any options, because menu entries are built each
* tick and the MouseListener runs on the awt thread
*/
@Subscribe
public void onClientTick(ClientTick event)
{
menuHasEntries = hasMenuEntries(client.getMenuEntries());
}
@Subscribe
private void onScriptPreFired(ScriptPreFired ev)
{
if (ev.getScriptId() == ScriptID.SETTINGS_SLIDER_CHOOSE_ONOP)
{
int arg = client.getIntStackSize() - 7;
int[] is = client.getIntStack();
if (is[arg] == SettingID.CAMERA_ZOOM)
{
//TODO: Implement client.createScriptEvent
//addZoomTooltip(client.getScriptActiveWidget());
}
}
}
@Subscribe
private void onWidgetLoaded(WidgetLoaded ev)
{
if (ev.getGroupId() == WidgetID.SETTINGS_SIDE_GROUP_ID)
{
addZoomTooltip(client.getWidget(WidgetInfo.SETTINGS_SIDE_CAMERA_ZOOM_SLIDER_TRACK));
}
}
private void addZoomTooltip(Widget w)
{
w.setOnMouseRepeatListener((JavaScriptCallback) ev ->
{
int value = client.getVar(VarClientInt.CAMERA_ZOOM_RESIZABLE_VIEWPORT);
int max = config.innerLimit() ? config.INNER_ZOOM_LIMIT : CameraPlugin.DEFAULT_INNER_ZOOM_LIMIT;
sliderTooltip = new Tooltip("Camera Zoom: " + value + " / " + max);
});
}
@Subscribe
private void onBeforeRender(BeforeRender ev)
{
if (sliderTooltip != null)
{
tooltipManager.add(sliderTooltip);
sliderTooltip = null;
}
}
/**
* The event that is triggered when a mouse button is pressed
* In this method the right click is changed to a middle-click to enable rotating the camera
* <p>
* This method also provides the config option to enable the middle-mouse button to always open the right click menu
*/
@Override
public MouseEvent mousePressed(MouseEvent mouseEvent)
{
if (SwingUtilities.isRightMouseButton(mouseEvent) && config.rightClickMovesCamera())
{
boolean oneButton = client.getVar(VarPlayer.MOUSE_BUTTONS) == 1;
// Only move the camera if there is nothing at the menu, or if
// in one-button mode. In one-button mode, left and right click always do the same thing,
// so always treat it as the menu is empty
if (!menuHasEntries || oneButton)
{
// Set the rightClick flag to true so we can release the button in mouseReleased() later
rightClick = true;
// Change the mousePressed() MouseEvent to the middle mouse button
mouseEvent = new MouseEvent((java.awt.Component) mouseEvent.getSource(),
mouseEvent.getID(),
mouseEvent.getWhen(),
mouseEvent.getModifiersEx(),
mouseEvent.getX(),
mouseEvent.getY(),
mouseEvent.getClickCount(),
mouseEvent.isPopupTrigger(),
MouseEvent.BUTTON2);
}
}
else if (SwingUtilities.isMiddleMouseButton((mouseEvent)) && config.middleClickMenu())
{
// Set the middleClick flag to true so we can release it later in mouseReleased()
middleClick = true;
// Chance the middle mouse button MouseEvent to a right-click
mouseEvent = new MouseEvent((java.awt.Component) mouseEvent.getSource(),
mouseEvent.getID(),
mouseEvent.getWhen(),
mouseEvent.getModifiersEx(),
mouseEvent.getX(),
mouseEvent.getY(),
mouseEvent.getClickCount(),
mouseEvent.isPopupTrigger(),
MouseEvent.BUTTON3);
}
return mouseEvent;
}
/**
* Correct the MouseEvent to release the correct button
*/
@Override
public MouseEvent mouseReleased(MouseEvent mouseEvent)
{
if (rightClick)
{
rightClick = false;
// Change the MouseEvent to button 2 so the middle mouse button will be released
mouseEvent = new MouseEvent((java.awt.Component) mouseEvent.getSource(),
mouseEvent.getID(),
mouseEvent.getWhen(),
mouseEvent.getModifiersEx(),
mouseEvent.getX(),
mouseEvent.getY(),
mouseEvent.getClickCount(),
mouseEvent.isPopupTrigger(),
MouseEvent.BUTTON2);
}
if (middleClick)
{
middleClick = false;
// Change the MouseEvent ot button 3 so the right mouse button will be released
mouseEvent = new MouseEvent((java.awt.Component) mouseEvent.getSource(),
mouseEvent.getID(),
mouseEvent.getWhen(),
mouseEvent.getModifiersEx(),
mouseEvent.getX(),
mouseEvent.getY(),
mouseEvent.getClickCount(),
mouseEvent.isPopupTrigger(),
MouseEvent.BUTTON3);
}
return mouseEvent;
}
/*
* These methods are unused but required to be present in a MouseListener implementation
*/
// region Unused MouseListener methods
@Override
public MouseEvent mouseDragged(MouseEvent mouseEvent)
{
return mouseEvent;
}
@Override
public MouseEvent mouseMoved(MouseEvent mouseEvent)
{
return mouseEvent;
}
@Override
public MouseEvent mouseClicked(MouseEvent mouseEvent)
{
return mouseEvent;
}
@Override
public MouseEvent mouseEntered(MouseEvent mouseEvent)
{
return mouseEvent;
}
@Override
public MouseEvent mouseExited(MouseEvent mouseEvent)
{
return mouseEvent;
}
// endregion
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.corp;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Polygon;
import javax.inject.Inject;
import net.runelite.api.NPC;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayUtil;
class CoreOverlay extends Overlay
{
private final CorpPlugin corpPlugin;
private final CorpConfig config;
@Inject
private CoreOverlay(CorpPlugin corpPlugin, CorpConfig corpConfig)
{
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_SCENE);
this.corpPlugin = corpPlugin;
this.config = corpConfig;
}
@Override
public Dimension render(Graphics2D graphics)
{
NPC core = corpPlugin.getCore();
if (core != null && config.markDarkCore())
{
Polygon canvasTilePoly = core.getCanvasTilePoly();
if (canvasTilePoly != null)
{
OverlayUtil.renderPolygon(graphics, canvasTilePoly, Color.RED.brighter());
}
}
return null;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.corp;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("corp")
public interface CorpConfig extends Config
{
@ConfigItem(
keyName = "showDamage",
name = "Show damage overlay",
description = "Show total damage overlay",
position = 0
)
default boolean showDamage()
{
return true;
}
@ConfigItem(
keyName = "markDarkCore",
name = "Mark dark core",
description = "Marks the dark energy core.",
position = 1
)
default boolean markDarkCore()
{
return true;
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.corp;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import javax.inject.Inject;
import net.runelite.api.Client;
import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG;
import net.runelite.api.NPC;
import net.runelite.api.Varbits;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.client.ui.overlay.OverlayLayer;
import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE;
import net.runelite.client.ui.overlay.OverlayMenuEntry;
import net.runelite.client.ui.overlay.OverlayPanel;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayPriority;
import net.runelite.client.ui.overlay.components.ComponentConstants;
import net.runelite.client.ui.overlay.components.LineComponent;
class CorpDamageOverlay extends OverlayPanel
{
private final Client client;
private final CorpPlugin corpPlugin;
private final CorpConfig config;
@Inject
private CorpDamageOverlay(Client client, CorpPlugin corpPlugin, CorpConfig config)
{
super(corpPlugin);
setPosition(OverlayPosition.TOP_LEFT);
setLayer(OverlayLayer.UNDER_WIDGETS);
setPriority(OverlayPriority.LOW);
this.client = client;
this.corpPlugin = corpPlugin;
this.config = config;
getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Corp overlay"));
}
@Override
public Dimension render(Graphics2D graphics)
{
Widget damageWidget = client.getWidget(WidgetID.CORP_DAMAGE, 0);
if (damageWidget != null)
{
damageWidget.setHidden(true);
}
NPC corp = corpPlugin.getCorp();
if (corp == null)
{
return null;
}
int myDamage = client.getVar(Varbits.CORP_DAMAGE);
int totalDamage = corpPlugin.getTotalDamage();
int players = corpPlugin.getPlayers().size();
// estimate how much damage is required for kill based on number of players
int damageForKill = players != 0 ? totalDamage / players : 0;
NPC core = corpPlugin.getCore();
if (core != null)
{
WorldPoint corePoint = core.getWorldLocation();
WorldPoint myPoint = client.getLocalPlayer().getWorldLocation();
String text = null;
if (core.getInteracting() == client.getLocalPlayer())
{
text = "The core is targeting you!";
}
else if (corePoint.distanceTo(myPoint) <= 1)
{
text = "Stay away from the core!";
}
if (text != null)
{
final FontMetrics fontMetrics = graphics.getFontMetrics();
int textWidth = Math.max(ComponentConstants.STANDARD_WIDTH, fontMetrics.stringWidth(text));
panelComponent.setPreferredSize(new Dimension(textWidth, 0));
panelComponent.getChildren().add(LineComponent.builder()
.left(text)
.leftColor(Color.RED)
.build());
}
}
if (config.showDamage())
{
panelComponent.getChildren().add(LineComponent.builder()
.left("Your damage")
.right(Integer.toString(myDamage))
.rightColor(damageForKill > 0 && myDamage >= damageForKill ? Color.GREEN : Color.RED)
.build());
panelComponent.getChildren().add(LineComponent.builder()
.left("Total damage")
.right(Integer.toString(totalDamage))
.build());
}
return super.render(graphics);
}
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.corp;
import com.google.inject.Provides;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Actor;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.NPC;
import net.runelite.api.NpcID;
import net.runelite.api.Varbits;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.HitsplatApplied;
import net.runelite.api.events.InteractingChanged;
import net.runelite.api.events.NpcDespawned;
import net.runelite.api.events.NpcSpawned;
import net.runelite.client.chat.ChatColorType;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.chat.QueuedMessage;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
@PluginDescriptor(
name = "Corporeal Beast",
description = "Show damage statistics and highlight dark energy cores",
tags = {"bosses", "combat", "pve", "overlay"}
)
@Slf4j
public class CorpPlugin extends Plugin
{
@Getter(AccessLevel.PACKAGE)
private NPC corp;
@Getter(AccessLevel.PACKAGE)
private NPC core;
private int yourDamage;
@Getter(AccessLevel.PACKAGE)
private int totalDamage;
@Getter(AccessLevel.PACKAGE)
private final Set<Actor> players = new HashSet<>();
@Inject
private Client client;
@Inject
private ChatMessageManager chatMessageManager;
@Inject
private OverlayManager overlayManager;
@Inject
private CorpDamageOverlay corpOverlay;
@Inject
private CoreOverlay coreOverlay;
@Provides
CorpConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(CorpConfig.class);
}
@Override
protected void startUp() throws Exception
{
overlayManager.add(corpOverlay);
overlayManager.add(coreOverlay);
}
@Override
protected void shutDown() throws Exception
{
overlayManager.remove(corpOverlay);
overlayManager.remove(coreOverlay);
corp = core = null;
yourDamage = 0;
totalDamage = 0;
players.clear();
}
@Subscribe
public void onGameStateChanged(GameStateChanged gameStateChanged)
{
if (gameStateChanged.getGameState() == GameState.LOADING)
{
players.clear();
}
}
@Subscribe
public void onNpcSpawned(NpcSpawned npcSpawned)
{
NPC npc = npcSpawned.getNpc();
switch (npc.getId())
{
case NpcID.CORPOREAL_BEAST:
log.debug("Corporeal beast spawn: {}", npc);
corp = npc;
yourDamage = 0;
totalDamage = 0;
players.clear();
break;
case NpcID.DARK_ENERGY_CORE:
core = npc;
break;
}
}
@Subscribe
public void onNpcDespawned(NpcDespawned npcDespawned)
{
NPC npc = npcDespawned.getNpc();
if (npc == corp)
{
log.debug("Corporeal beast despawn: {}", npc);
corp = null;
players.clear();
if (npc.isDead())
{
// Show kill stats
String message = new ChatMessageBuilder()
.append(ChatColorType.NORMAL)
.append("Corporeal Beast: Your damage: ")
.append(ChatColorType.HIGHLIGHT)
.append(Integer.toString(yourDamage))
.append(ChatColorType.NORMAL)
.append(", Total damage: ")
.append(ChatColorType.HIGHLIGHT)
.append(Integer.toString(totalDamage))
.build();
chatMessageManager.queue(QueuedMessage.builder()
.type(ChatMessageType.CONSOLE)
.runeLiteFormattedMessage(message)
.build());
}
}
else if (npc == core)
{
core = null;
}
}
@Subscribe
public void onHitsplatApplied(HitsplatApplied hitsplatApplied)
{
Actor actor = hitsplatApplied.getActor();
if (actor != corp)
{
return;
}
int myDamage = client.getVar(Varbits.CORP_DAMAGE);
// sometimes hitsplats are applied after the damage counter has been reset
if (myDamage > 0)
{
yourDamage = myDamage;
}
totalDamage += hitsplatApplied.getHitsplat().getAmount();
}
@Subscribe
public void onInteractingChanged(InteractingChanged interactingChanged)
{
Actor source = interactingChanged.getSource();
Actor target = interactingChanged.getTarget();
if (corp == null || target != corp)
{
return;
}
players.add(source);
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing;
import com.google.gson.Gson;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.runelite.http.api.RuneLiteAPI;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
@Slf4j
@Singleton
public class CrowdsourcingManager
{
private static final String CROWDSOURCING_BASE = "https://crowdsource.runescape.wiki/runelite";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final Gson GSON = RuneLiteAPI.GSON;
@Inject
private OkHttpClient okHttpClient;
private List<Object> data = new ArrayList<>();
public void storeEvent(Object event)
{
synchronized (this)
{
data.add(event);
}
}
protected void submitToAPI()
{
List<Object> temp;
synchronized (this)
{
if (data.isEmpty())
{
return;
}
temp = data;
data = new ArrayList<>();
}
Request r = new Request.Builder()
.url(CROWDSOURCING_BASE)
.post(RequestBody.create(JSON, GSON.toJson(temp)))
.build();
okHttpClient.newCall(r).enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e)
{
log.debug("Error sending crowdsourcing data", e);
}
@Override
public void onResponse(Call call, Response response)
{
log.debug("Successfully sent crowdsourcing data");
response.close();
}
});
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing;
import java.time.temporal.ChronoUnit;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.crowdsourcing.cooking.CrowdsourcingCooking;
import net.runelite.client.plugins.crowdsourcing.dialogue.CrowdsourcingDialogue;
import net.runelite.client.plugins.crowdsourcing.movement.CrowdsourcingMovement;
import net.runelite.client.plugins.crowdsourcing.music.CrowdsourcingMusic;
import net.runelite.client.plugins.crowdsourcing.thieving.CrowdsourcingThieving;
import net.runelite.client.plugins.crowdsourcing.woodcutting.CrowdsourcingWoodcutting;
import net.runelite.client.plugins.crowdsourcing.zmi.CrowdsourcingZMI;
import net.runelite.client.task.Schedule;
@Slf4j
@PluginDescriptor(
name = "OSRS Wiki Crowdsourcing",
description = "Send data to the wiki to help figure out skilling success rates, burn rates, more. See osrs.wiki/RS:CROWD"
)
public class CrowdsourcingPlugin extends Plugin
{
// Number of seconds to wait between trying to send data to the wiki.
private static final int SECONDS_BETWEEN_UPLOADS = 300;
@Inject
private EventBus eventBus;
@Inject
private CrowdsourcingManager manager;
@Inject
private CrowdsourcingCooking cooking;
@Inject
private CrowdsourcingDialogue dialogue;
@Inject
private CrowdsourcingMovement movement;
@Inject
private CrowdsourcingMusic music;
@Inject
private CrowdsourcingThieving thieving;
@Inject
private CrowdsourcingWoodcutting woodcutting;
@Inject
private CrowdsourcingZMI zmi;
@Override
protected void startUp() throws Exception
{
eventBus.register(cooking);
eventBus.register(dialogue);
eventBus.register(movement);
eventBus.register(music);
eventBus.register(thieving);
eventBus.register(woodcutting);
eventBus.register(zmi);
}
@Override
protected void shutDown() throws Exception
{
eventBus.unregister(cooking);
eventBus.unregister(dialogue);
eventBus.unregister(movement);
eventBus.unregister(music);
eventBus.unregister(thieving);
eventBus.unregister(woodcutting);
eventBus.unregister(zmi);
}
@Schedule(
period = SECONDS_BETWEEN_UPLOADS,
unit = ChronoUnit.SECONDS,
asynchronous = true
)
public void submitToAPI()
{
manager.submitToAPI();
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.cooking;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class CookingData
{
private final String message;
private final boolean hasCookingGauntlets;
private final boolean inHosidiusKitchen;
private final boolean kourendElite;
private final int lastGameObjectClicked;
private final int level;
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.cooking;
import javax.inject.Inject;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
import net.runelite.api.ItemContainer;
import net.runelite.api.ItemID;
import net.runelite.api.MenuAction;
import net.runelite.api.Player;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
public class CrowdsourcingCooking
{
private static final int HOSIDIUS_KITCHEN_REGION = 6712;
@Inject
private CrowdsourcingManager manager;
@Inject
private Client client;
private int lastGameObjectClicked;
private boolean hasCookingGauntlets()
{
ItemContainer equipmentContainer = client.getItemContainer(InventoryID.EQUIPMENT);
if (equipmentContainer == null)
{
return false;
}
return equipmentContainer.contains(ItemID.COOKING_GAUNTLETS);
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
if (event.getType() != ChatMessageType.SPAM)
{
return;
}
final String message = event.getMessage();
// Message prefixes taken from CookingPlugin
if (message.startsWith("You successfully cook")
|| message.startsWith("You successfully bake")
|| message.startsWith("You manage to cook")
|| message.startsWith("You roast a")
|| message.startsWith("You cook")
|| message.startsWith("You accidentally burn")
|| message.startsWith("You accidentally spoil"))
{
boolean inHosidiusKitchen = false;
Player local = client.getLocalPlayer();
if (local != null && local.getWorldLocation().getRegionID() == HOSIDIUS_KITCHEN_REGION)
{
inHosidiusKitchen = true;
}
int cookingLevel = client.getBoostedSkillLevel(Skill.COOKING);
boolean hasCookingGauntlets = hasCookingGauntlets();
boolean kourendElite = client.getVar(Varbits.DIARY_KOUREND_ELITE) == 1;
CookingData data = new CookingData(message, hasCookingGauntlets, inHosidiusKitchen, kourendElite, lastGameObjectClicked, cookingLevel);
manager.storeEvent(data);
}
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
{
MenuAction action = menuOptionClicked.getMenuAction();
if (action == MenuAction.ITEM_USE_ON_GAME_OBJECT
|| action == MenuAction.GAME_OBJECT_FIRST_OPTION
|| action == MenuAction.GAME_OBJECT_SECOND_OPTION
|| action == MenuAction.GAME_OBJECT_THIRD_OPTION
|| action == MenuAction.GAME_OBJECT_FOURTH_OPTION
|| action == MenuAction.GAME_OBJECT_FIFTH_OPTION)
{
lastGameObjectClicked = menuOptionClicked.getId();
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.dialogue;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.events.GameTick;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
public class CrowdsourcingDialogue
{
private static final String USERNAME_TOKEN = "%USERNAME%";
@Inject
private Client client;
@Inject
private CrowdsourcingManager manager;
private String lastNpcDialogueText = null;
private String lastPlayerDialogueText = null;
private Widget[] dialogueOptions;
private String sanitize(String dialogue)
{
String username = client.getLocalPlayer().getName();
return dialogue.replaceAll(username, USERNAME_TOKEN);
}
@Subscribe
public void onGameTick(GameTick tick)
{
Widget npcDialogueTextWidget = client.getWidget(WidgetInfo.DIALOG_NPC_TEXT);
if (npcDialogueTextWidget != null && !npcDialogueTextWidget.getText().equals(lastNpcDialogueText))
{
lastNpcDialogueText = npcDialogueTextWidget.getText();
String npcName = client.getWidget(WidgetInfo.DIALOG_NPC_NAME).getText();
NpcDialogueData data = new NpcDialogueData(sanitize(lastNpcDialogueText), npcName);
manager.storeEvent(data);
}
Widget playerDialogueTextWidget = client.getWidget(WidgetID.DIALOG_PLAYER_GROUP_ID, 4);
if (playerDialogueTextWidget != null && !playerDialogueTextWidget.getText().equals(lastPlayerDialogueText))
{
lastPlayerDialogueText = playerDialogueTextWidget.getText();
PlayerDialogueData data = new PlayerDialogueData(sanitize(lastPlayerDialogueText));
manager.storeEvent(data);
}
Widget playerDialogueOptionsWidget = client.getWidget(WidgetID.DIALOG_OPTION_GROUP_ID, 1);
if (playerDialogueOptionsWidget != null && playerDialogueOptionsWidget.getChildren() != dialogueOptions)
{
dialogueOptions = playerDialogueOptionsWidget.getChildren();
String[] optionsText = new String[dialogueOptions.length];
for (int i = 0; i < dialogueOptions.length; i++)
{
optionsText[i] = sanitize(dialogueOptions[i].getText());
}
DialogueOptionsData data = new DialogueOptionsData(optionsText);
manager.storeEvent(data);
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.dialogue;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class DialogueOptionsData
{
private final String[] options;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.dialogue;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class NpcDialogueData
{
private final String message;
private final String name;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.dialogue;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class PlayerDialogueData
{
private final String message;
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.movement;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.MenuAction;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
public class CrowdsourcingMovement
{
@Inject
private Client client;
@Inject
private CrowdsourcingManager manager;
private WorldPoint lastPoint;
private int ticksStill;
private boolean lastIsInInstance;
private MenuOptionClicked lastClick;
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
{
if (menuOptionClicked.getMenuAction() != MenuAction.WALK
&& !menuOptionClicked.getMenuOption().equals("Message"))
{
lastClick = menuOptionClicked;
}
}
@Subscribe
public void onGameTick(GameTick tick)
{
LocalPoint local = LocalPoint.fromWorld(client, client.getLocalPlayer().getWorldLocation());
WorldPoint nextPoint = WorldPoint.fromLocalInstance(client, local);
boolean nextIsInInstance = client.isInInstancedRegion();
if (lastPoint != null)
{
int distance = nextPoint.distanceTo(lastPoint);
if (distance > 2 || nextIsInInstance != lastIsInInstance)
{
MovementData data = new MovementData(lastPoint, nextPoint, lastIsInInstance, nextIsInInstance, ticksStill, lastClick);
manager.storeEvent(data);
}
if (distance > 0)
{
ticksStill = 0;
}
}
ticksStill++;
lastPoint = nextPoint;
lastIsInInstance = nextIsInInstance;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.movement;
import lombok.AllArgsConstructor;
import lombok.Data;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.MenuOptionClicked;
@Data
@AllArgsConstructor
public class MovementData
{
private final WorldPoint start;
private final WorldPoint end;
private final boolean fromInstance;
private final boolean toInstance;
private final int ticks;
private MenuOptionClicked lastClick;
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.music;
import javax.inject.Inject;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
public class CrowdsourcingMusic
{
private static final String MUSIC_UNLOCK_MESSAGE = "You have unlocked a new music track:";
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private CrowdsourcingManager manager;
@Subscribe
public void onChatMessage(ChatMessage event)
{
if (event.getType() == ChatMessageType.GAMEMESSAGE)
{
String message = event.getMessage();
if (message.contains(MUSIC_UNLOCK_MESSAGE))
{
// Need to delay this by a tick because the location is not set until after the message
clientThread.invokeLater(() ->
{
LocalPoint local = LocalPoint.fromWorld(client, client.getLocalPlayer().getWorldLocation());
WorldPoint location = WorldPoint.fromLocalInstance(client, local);
boolean isInInstance = client.isInInstancedRegion();
MusicUnlockData data = new MusicUnlockData(location, isInInstance, message);
manager.storeEvent(data);
});
}
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.music;
import lombok.AllArgsConstructor;
import lombok.Data;
import net.runelite.api.coords.WorldPoint;
@Data
@AllArgsConstructor
public class MusicUnlockData
{
private final WorldPoint location;
private final boolean isInInstance;
private final String message;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, TheStonedTurtle <https://github.com/TheStonedTurtle>
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -22,13 +22,12 @@
* (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.bank;
import lombok.Value;
package net.runelite.client.plugins.crowdsourcing.skilling;
@Value
class ContainerPrices
public enum SkillingEndReason
{
private long gePrice;
private long highAlchPrice;
DEPLETED,
INVENTORY_FULL,
INTERRUPTED
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.skilling;
public enum SkillingState
{
READY,
CLICKED,
CUTTING,
RECOVERING
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.thieving;
import java.util.regex.Pattern;
import javax.inject.Inject;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
import net.runelite.api.ItemContainer;
import net.runelite.api.ItemID;
import net.runelite.api.NPC;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
public class CrowdsourcingThieving
{
private static final String BLACKJACK_SUCCESS = "You smack the bandit over the head and render them unconscious.";
private static final String BLACKJACK_FAIL = "Your blow only glances off the bandit's head.";
private static final Pattern PICKPOCKET_SUCCESS = Pattern.compile("You pick .*'s pocket\\.");
private static final Pattern PICKPOCKET_FAIL = Pattern.compile("You fail to pick .*'s pocket\\.");
@Inject
private Client client;
@Inject
private CrowdsourcingManager manager;
private int lastPickpocketTarget;
private boolean hasGlovesOfSilence()
{
ItemContainer equipmentContainer = client.getItemContainer(InventoryID.EQUIPMENT);
if (equipmentContainer == null)
{
return false;
}
return equipmentContainer.contains(ItemID.GLOVES_OF_SILENCE);
}
private boolean hasThievingCape()
{
ItemContainer equipmentContainer = client.getItemContainer(InventoryID.EQUIPMENT);
if (equipmentContainer == null)
{
return false;
}
return equipmentContainer.contains(ItemID.THIEVING_CAPE) ||
equipmentContainer.contains(ItemID.THIEVING_CAPET) ||
equipmentContainer.contains(ItemID.MAX_CAPE);
}
private int getArdougneDiary()
{
int easy = client.getVar(Varbits.DIARY_ARDOUGNE_EASY);
int medium = client.getVar(Varbits.DIARY_ARDOUGNE_MEDIUM);
int hard = client.getVar(Varbits.DIARY_ARDOUGNE_HARD);
int elite = client.getVar(Varbits.DIARY_ARDOUGNE_ELITE);
return easy + 2 * medium + 4 * hard + 8 * elite;
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
if (event.getType() != ChatMessageType.SPAM)
{
return;
}
String message = event.getMessage();
if (BLACKJACK_SUCCESS.equals(message)
|| BLACKJACK_FAIL.equals(message)
|| PICKPOCKET_FAIL.matcher(message).matches()
|| PICKPOCKET_SUCCESS.matcher(message).matches())
{
WorldPoint location = client.getLocalPlayer().getWorldLocation();
int ardougneDiary = getArdougneDiary();
boolean silence = hasGlovesOfSilence();
boolean thievingCape = hasThievingCape();
int thievingLevel = client.getBoostedSkillLevel(Skill.THIEVING);
PickpocketData data = new PickpocketData(thievingLevel, lastPickpocketTarget, message, location, silence, thievingCape, ardougneDiary);
manager.storeEvent(data);
}
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked event)
{
if (event.getMenuOption().equals("Pickpocket") || event.getMenuOption().equals("Knock-Out"))
{
NPC[] cachedNPCs = client.getCachedNPCs();
NPC npc = cachedNPCs[event.getId()];
lastPickpocketTarget = npc.getId();
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.thieving;
import lombok.AllArgsConstructor;
import lombok.Data;
import net.runelite.api.coords.WorldPoint;
@Data
@AllArgsConstructor
public class PickpocketData
{
private final int level;
private final int target;
private final String message;
private final WorldPoint location;
private final boolean silence;
private final boolean thievingCape;
private final int ardougneDiary;
}

View File

@@ -0,0 +1,233 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.woodcutting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import net.runelite.api.AnimationID;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.ItemID;
import net.runelite.api.MenuAction;
import net.runelite.api.ObjectID;
import net.runelite.api.Skill;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameObjectDespawned;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
import net.runelite.client.plugins.crowdsourcing.skilling.SkillingEndReason;
import net.runelite.client.plugins.crowdsourcing.skilling.SkillingState;
public class CrowdsourcingWoodcutting
{
private static final String CHOPPING_MESSAGE = "You swing your axe at the tree.";
private static final String INVENTORY_FULL_MESSAGE = "Your inventory is too full to hold any more logs.";
private static final String NEST_MESSAGE = "A bird's nest falls out of the tree";
private static final Set<Integer> TREE_OBJECTS = new ImmutableSet.Builder<Integer>().
add(ObjectID.OAK_10820).
add(ObjectID.YEW).
add(ObjectID.TREE).
add(ObjectID.TREE_1278).
add(ObjectID.WILLOW).
add(ObjectID.WILLOW_10829).
add(ObjectID.WILLOW_10831).
add(ObjectID.WILLOW_10833).
add(ObjectID.SCRAPEY_TREE).
add(ObjectID.JUNGLE_TREE_15951).
add(ObjectID.JUNGLE_TREE_15954).
add(ObjectID.JUNGLE_TREE_15948).
add(ObjectID.MAPLE_TREE_10832).
add(ObjectID.MAHOGANY).
add(ObjectID.TEAK).
add(ObjectID.MAGIC_TREE_10834).
add(ObjectID.HOLLOW_TREE_10821).
add(ObjectID.HOLLOW_TREE_10830).
add(ObjectID.ACHEY_TREE).
build();
private static final Map<Integer, Integer> AXE_ANIMS = new ImmutableMap.Builder<Integer, Integer>().
put(AnimationID.WOODCUTTING_BRONZE, ItemID.BRONZE_AXE).
put(AnimationID.WOODCUTTING_IRON, ItemID.IRON_AXE).
put(AnimationID.WOODCUTTING_STEEL, ItemID.STEEL_AXE).
put(AnimationID.WOODCUTTING_BLACK, ItemID.BLACK_AXE).
put(AnimationID.WOODCUTTING_MITHRIL, ItemID.MITHRIL_AXE).
put(AnimationID.WOODCUTTING_ADAMANT, ItemID.ADAMANT_AXE).
put(AnimationID.WOODCUTTING_RUNE, ItemID.RUNE_AXE).
put(AnimationID.WOODCUTTING_DRAGON, ItemID.DRAGON_AXE).
put(AnimationID.WOODCUTTING_INFERNAL, ItemID.INFERNAL_AXE).
put(AnimationID.WOODCUTTING_3A_AXE, ItemID._3RD_AGE_AXE).
put(AnimationID.WOODCUTTING_CRYSTAL, ItemID.CRYSTAL_AXE).
put(AnimationID.WOODCUTTING_TRAILBLAZER, ItemID.TRAILBLAZER_AXE).build();
private static final Set<String> SUCCESS_MESSAGES = new ImmutableSet.Builder<String>().
add("You get some logs.").
add("You get some oak logs.").
add("You get some willow logs.").
add("You get some teak logs.").
add("You get some teak logs and give them to Carpenter Kjallak.").
add("You get some maple logs.").
add("You get some maple logs and give them to Lumberjack Leif.").
add("You get some mahogany logs.").
add("You get some mahogany logs and give them to Carpenter Kjallak.").
add("You get some yew logs.").
add("You get some magic logs.").
add("You get some scrapey tree logs.").
add("You get some bark.").
build();
@Inject
private CrowdsourcingManager manager;
@Inject
private Client client;
private SkillingState state = SkillingState.RECOVERING;
private int lastExperimentEnd = 0;
private WorldPoint treeLocation;
private int treeId;
private int startTick;
private int axe;
private List<Integer> chopTicks = new ArrayList<>();
private List<Integer> nestTicks = new ArrayList<>();
private void endExperiment(SkillingEndReason reason)
{
if (state == SkillingState.CUTTING)
{
int endTick = client.getTickCount();
int woodcuttingLevel = client.getBoostedSkillLevel(Skill.WOODCUTTING);
WoodcuttingData data = new WoodcuttingData(
woodcuttingLevel,
startTick,
endTick,
chopTicks,
nestTicks,
axe,
treeId,
treeLocation,
reason
);
manager.storeEvent(data);
chopTicks = new ArrayList<>();
nestTicks = new ArrayList<>();
}
state = SkillingState.RECOVERING;
lastExperimentEnd = client.getTickCount();
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
final String message = event.getMessage();
final ChatMessageType type = event.getType();
if (state == SkillingState.CLICKED && type == ChatMessageType.SPAM && message.equals(CHOPPING_MESSAGE))
{
startTick = client.getTickCount();
state = SkillingState.CUTTING;
}
else if (state == SkillingState.CUTTING && type == ChatMessageType.SPAM && SUCCESS_MESSAGES.contains(message))
{
chopTicks.add(client.getTickCount());
}
else if (state == SkillingState.CUTTING && type == ChatMessageType.GAMEMESSAGE && message.equals(INVENTORY_FULL_MESSAGE))
{
endExperiment(SkillingEndReason.INVENTORY_FULL);
}
else if (state == SkillingState.CUTTING && type == ChatMessageType.GAMEMESSAGE && message.contains(NEST_MESSAGE))
{
nestTicks.add(client.getTickCount());
}
}
@Subscribe
public void onGameTick(GameTick tick)
{
int animId = client.getLocalPlayer().getAnimation();
if (state == SkillingState.CUTTING)
{
if (AXE_ANIMS.containsKey(animId))
{
axe = AXE_ANIMS.get(animId);
}
else
{
endExperiment(SkillingEndReason.INTERRUPTED);
}
}
else if (animId != -1)
{
endExperiment(SkillingEndReason.INTERRUPTED);
}
else if (state == SkillingState.RECOVERING && client.getTickCount() - lastExperimentEnd >= 4)
{
state = SkillingState.READY;
}
else if (state == SkillingState.CLICKED && client.getTickCount() - lastExperimentEnd >= 20)
{
state = SkillingState.READY;
}
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
{
MenuAction menuAction = menuOptionClicked.getMenuAction();
int id = menuOptionClicked.getId();
if (state == SkillingState.READY && menuAction == MenuAction.GAME_OBJECT_FIRST_OPTION && TREE_OBJECTS.contains(id))
{
state = SkillingState.CLICKED;
lastExperimentEnd = client.getTickCount();
treeId = id;
treeLocation = WorldPoint.fromScene(client, menuOptionClicked.getActionParam(), menuOptionClicked.getWidgetId(), client.getPlane());
}
else
{
endExperiment(SkillingEndReason.INTERRUPTED);
}
}
@Subscribe
public void onGameObjectDespawned(GameObjectDespawned event)
{
if (state != SkillingState.CUTTING)
{
return;
}
if (treeId == event.getGameObject().getId() && treeLocation.equals(event.getTile().getWorldLocation()))
{
endExperiment(SkillingEndReason.DEPLETED);
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.woodcutting;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import net.runelite.api.coords.WorldPoint;
import net.runelite.client.plugins.crowdsourcing.skilling.SkillingEndReason;
@Data
@AllArgsConstructor
public class WoodcuttingData
{
private final int level;
private final int startTick;
private final int endTick;
private final List<Integer> chopTicks;
private final List<Integer> nestTicks;
private final int axe;
private final int treeId;
private final WorldPoint treeLocation;
private final SkillingEndReason reason;
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.zmi;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multisets;
import java.util.Arrays;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
import net.runelite.api.ItemContainer;
import net.runelite.api.MenuAction;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.events.StatChanged;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.crowdsourcing.CrowdsourcingManager;
public class CrowdsourcingZMI
{
private static final String CHAT_MESSAGE_ZMI = "You bind the temple's power into runes.";
@Inject
private CrowdsourcingManager manager;
@Inject
private Client client;
private int gameTickZMI = -1;
private int illegalActionTick = -1;
private int previousRunecraftXp = 0;
private int runecraftXpGained = 0;
private Multiset<Integer> previousInventorySnapshot;
private Multiset<Integer> getInventorySnapshot()
{
final ItemContainer inventory = client.getItemContainer(InventoryID.INVENTORY);
Multiset<Integer> inventorySnapshot = HashMultiset.create();
if (inventory != null)
{
Arrays.stream(inventory.getItems())
.forEach(item -> inventorySnapshot.add(item.getId(), item.getQuantity()));
}
return inventorySnapshot;
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
{
MenuAction action = menuOptionClicked.getMenuAction();
switch (action)
{
case ITEM_FIRST_OPTION:
case ITEM_SECOND_OPTION:
case ITEM_THIRD_OPTION:
case ITEM_FOURTH_OPTION:
case ITEM_FIFTH_OPTION:
case GROUND_ITEM_FIRST_OPTION:
case GROUND_ITEM_SECOND_OPTION:
case GROUND_ITEM_THIRD_OPTION:
case GROUND_ITEM_FOURTH_OPTION:
case GROUND_ITEM_FIFTH_OPTION:
case ITEM_DROP:
illegalActionTick = client.getTickCount();
break;
}
}
@Subscribe
public void onChatMessage(ChatMessage chatMessage)
{
if (chatMessage.getMessage().equals(CHAT_MESSAGE_ZMI))
{
gameTickZMI = client.getTickCount();
previousRunecraftXp = client.getSkillExperience(Skill.RUNECRAFT);
previousInventorySnapshot = getInventorySnapshot();
}
}
@Subscribe
public void onStatChanged(StatChanged statChanged)
{
if (gameTickZMI == client.getTickCount())
{
int currentRunecraftXp = statChanged.getXp();
runecraftXpGained = currentRunecraftXp - previousRunecraftXp;
}
}
@Subscribe
public void onItemContainerChanged(ItemContainerChanged event)
{
int itemContainerChangedTick = client.getTickCount();
if (event.getItemContainer() != client.getItemContainer(InventoryID.INVENTORY) || gameTickZMI != itemContainerChangedTick)
{
return;
}
int tickDelta = itemContainerChangedTick - illegalActionTick;
boolean ardougneMedium = client.getVar(Varbits.DIARY_ARDOUGNE_MEDIUM) == 1;
int runecraftBoostedLevel = client.getBoostedSkillLevel(Skill.RUNECRAFT);
Multiset<Integer> currentInventorySnapshot = getInventorySnapshot();
final Multiset<Integer> itemsReceived = Multisets.difference(currentInventorySnapshot, previousInventorySnapshot);
final Multiset<Integer> itemsRemoved = Multisets.difference(previousInventorySnapshot, currentInventorySnapshot);
ZMIData data = new ZMIData(tickDelta, ardougneMedium, runecraftBoostedLevel, runecraftXpGained, itemsReceived, itemsRemoved);
manager.storeEvent(data);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2019, Weird Gloop <admin@weirdgloop.org>
* 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.crowdsourcing.zmi;
import com.google.common.collect.Multiset;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ZMIData
{
private final int tickDelta;
private final boolean ardougneMedium;
private final int level;
private final int xpGained;
private final Multiset<Integer> itemsReceived;
private final Multiset<Integer> itemsRemoved;
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2018, Kruithne <kruithne@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.customcursor;
import java.awt.image.BufferedImage;
import javax.annotation.Nullable;
import lombok.Getter;
import net.runelite.client.util.ImageUtil;
@Getter
public enum CustomCursor
{
RS3_GOLD("RS3 Gold", "cursor-rs3-gold.png"),
RS3_SILVER("RS3 Silver", "cursor-rs3-silver.png"),
DRAGON_DAGGER("Dragon Dagger", "cursor-dragon-dagger.png"),
DRAGON_DAGGER_POISON("Dragon Dagger (p)", "cursor-dragon-dagger-p.png"),
TROUT("Trout", "cursor-trout.png"),
DRAGON_SCIMITAR("Dragon Scimitar", "cursor-dragon-scimitar.png"),
EQUIPPED_WEAPON("Equipped Weapon"),
CUSTOM_IMAGE("Custom Image");
private final String name;
@Nullable
private final BufferedImage cursorImage;
CustomCursor(String name)
{
this.name = name;
this.cursorImage = null;
}
CustomCursor(String name, String icon)
{
this.name = name;
this.cursorImage = ImageUtil.getResourceStreamFromClass(CustomCursorPlugin.class, icon);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2018, Kruithne <kruithne@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.customcursor;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("customcursor")
public interface CustomCursorConfig extends Config
{
@ConfigItem(
keyName = "cursorStyle",
name = "Cursor",
description = "Select which cursor you wish to use"
)
default CustomCursor selectedCursor()
{
return CustomCursor.RS3_GOLD;
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (c) 2018, Kruithne <kruithne@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.customcursor;
import com.google.inject.Provides;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.client.RuneLite;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.ClientUI;
@PluginDescriptor(
name = "Custom Cursor",
description = "Replaces your mouse cursor image",
enabledByDefault = false
)
@Slf4j
public class CustomCursorPlugin extends Plugin
{
private static final File CUSTOM_IMAGE_FILE = new File(RuneLite.RUNELITE_DIR, "cursor.png");
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ClientUI clientUI;
@Inject
private CustomCursorConfig config;
@Inject
private ItemManager itemManager;
@Provides
CustomCursorConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(CustomCursorConfig.class);
}
@Override
protected void startUp()
{
updateCursor();
}
@Override
protected void shutDown()
{
clientUI.resetCursor();
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("customcursor") && event.getKey().equals("cursorStyle"))
{
updateCursor();
}
}
@Subscribe
public void onItemContainerChanged(ItemContainerChanged event)
{
if (config.selectedCursor() == CustomCursor.EQUIPPED_WEAPON && event.getContainerId() == InventoryID.EQUIPMENT.getId())
{
updateCursor();
}
}
private void updateCursor()
{
CustomCursor selectedCursor = config.selectedCursor();
if (selectedCursor == CustomCursor.CUSTOM_IMAGE)
{
if (CUSTOM_IMAGE_FILE.exists())
{
try
{
BufferedImage image;
synchronized (ImageIO.class)
{
image = ImageIO.read(CUSTOM_IMAGE_FILE);
}
clientUI.setCursor(image, selectedCursor.getName());
}
catch (Exception e)
{
log.error("error setting custom cursor", e);
clientUI.resetCursor();
}
}
else
{
clientUI.resetCursor();
}
}
else if (selectedCursor == CustomCursor.EQUIPPED_WEAPON)
{
clientThread.invokeLater(() ->
{
final ItemContainer equipment = client.getItemContainer(InventoryID.EQUIPMENT);
if (equipment == null)
{
clientUI.resetCursor();
return;
}
Item weapon = equipment.getItem(EquipmentInventorySlot.WEAPON.getSlotIdx());
if (weapon == null)
{
clientUI.resetCursor();
return;
}
final BufferedImage image = itemManager.getImage(weapon.getId());
if (weapon.getQuantity() > 0)
{
clientUI.setCursor(image, selectedCursor.getName());
}
else
{
clientUI.resetCursor();
}
});
}
else
{
assert selectedCursor.getCursorImage() != null;
clientUI.setCursor(selectedCursor.getCursorImage(), selectedCursor.getName());
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (c) 2018, Infinitay <https://github.com/Infinitay>
* Copyright (c) 2018, Shaun Dreclin <https://github.com/ShaunDreclin>
*
* 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.dailytaskindicators;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("dailytaskindicators")
public interface DailyTasksConfig extends Config
{
@ConfigItem(
position = 1,
keyName = "showHerbBoxes",
name = "Show Herb Boxes",
description = "Show a message when you can collect your daily herb boxes at NMZ."
)
default boolean showHerbBoxes()
{
return true;
}
@ConfigItem(
position = 2,
keyName = "showStaves",
name = "Show Claimable Staves",
description = "Show a message when you can collect your daily battlestaves from Zaff."
)
default boolean showStaves()
{
return true;
}
@ConfigItem(
position = 3,
keyName = "showEssence",
name = "Show Claimable Essence",
description = "Show a message when you can collect your daily pure essence from Wizard Cromperty."
)
default boolean showEssence()
{
return false;
}
@ConfigItem(
position = 4,
keyName = "showRunes",
name = "Show Claimable Random Runes",
description = "Show a message when you can collect your daily random runes from Lundail."
)
default boolean showRunes()
{
return false;
}
@ConfigItem(
position = 5,
keyName = "showSand",
name = "Show Claimable Sand",
description = "Show a message when you can collect your daily sand from Bert."
)
default boolean showSand()
{
return false;
}
@ConfigItem(
position = 6,
keyName = "showFlax",
name = "Show Claimable Bow Strings",
description = "Show a message when you can convert noted flax to bow strings with the Flax keeper."
)
default boolean showFlax()
{
return false;
}
@ConfigItem(
position = 7,
keyName = "showBonemeal",
name = "Show Claimable Bonemeal & Slime",
description = "Show a message when you can collect bonemeal & slime from Robin."
)
default boolean showBonemeal()
{
return false;
}
@ConfigItem(
position = 8,
keyName = "showDynamite",
name = "Show Claimable Dynamite",
description = "Show a message when you can collect Dynamite from Thirus."
)
default boolean showDynamite()
{
return false;
}
@ConfigItem(
position = 9,
keyName = "showArrows",
name = "Show Claimable Ogre Arrows",
description = "Show a message when you can collect ogre arrows from Rantz."
)
default boolean showArrows()
{
return false;
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright (c) 2018, Infinitay <https://github.com/Infinitay>
* Copyright (c) 2018-2019, Shaun Dreclin <https://github.com/ShaunDreclin>
* 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.dailytaskindicators;
import com.google.inject.Provides;
import javax.inject.Inject;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.VarClientInt;
import net.runelite.api.VarPlayer;
import net.runelite.api.Varbits;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.vars.AccountType;
import net.runelite.client.chat.ChatColorType;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.chat.QueuedMessage;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
@PluginDescriptor(
name = "Daily Task Indicator",
description = "Show chat notifications for daily tasks upon login"
)
public class DailyTasksPlugin extends Plugin
{
private static final int ONE_DAY = 86400000;
private static final String HERB_BOX_MESSAGE = "You have herb boxes waiting to be collected at NMZ.";
private static final int HERB_BOX_MAX = 15;
private static final int HERB_BOX_COST = 9500;
private static final String STAVES_MESSAGE = "You have battlestaves waiting to be collected from Zaff.";
private static final String ESSENCE_MESSAGE = "You have essence waiting to be collected from Wizard Cromperty.";
private static final String RUNES_MESSAGE = "You have random runes waiting to be collected from Lundail.";
private static final String SAND_MESSAGE = "You have sand waiting to be collected from Bert.";
private static final int SAND_QUEST_COMPLETE = 160;
private static final String FLAX_MESSAGE = "You have bowstrings waiting to be converted from flax from the Flax keeper.";
private static final String ARROWS_MESSAGE = "You have ogre arrows waiting to be collected from Rantz.";
private static final String BONEMEAL_MESSAGE = "You have bonemeal and slime waiting to be collected from Robin.";
private static final int BONEMEAL_PER_DIARY = 13;
private static final String DYNAMITE_MESSAGE = "You have dynamite waiting to be collected from Thirus.";
@Inject
private Client client;
@Inject
private DailyTasksConfig config;
@Inject
private ChatMessageManager chatMessageManager;
private long lastReset;
private boolean loggingIn;
@Provides
DailyTasksConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(DailyTasksConfig.class);
}
@Override
public void startUp()
{
loggingIn = true;
}
@Override
public void shutDown()
{
lastReset = 0L;
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState() == GameState.LOGGING_IN)
{
loggingIn = true;
}
}
@Subscribe
public void onGameTick(GameTick event)
{
long currentTime = System.currentTimeMillis();
boolean dailyReset = !loggingIn && currentTime - lastReset > ONE_DAY;
if ((dailyReset || loggingIn)
&& client.getVar(VarClientInt.MEMBERSHIP_STATUS) == 1)
{
// Round down to the nearest day
lastReset = (long) Math.floor(currentTime / ONE_DAY) * ONE_DAY;
loggingIn = false;
if (config.showHerbBoxes())
{
checkHerbBoxes(dailyReset);
}
if (config.showStaves())
{
checkStaves(dailyReset);
}
if (config.showEssence())
{
checkEssence(dailyReset);
}
if (config.showRunes())
{
checkRunes(dailyReset);
}
if (config.showSand())
{
checkSand(dailyReset);
}
if (config.showFlax())
{
checkFlax(dailyReset);
}
if (config.showBonemeal())
{
checkBonemeal(dailyReset);
}
if (config.showDynamite())
{
checkDynamite(dailyReset);
}
if (config.showArrows())
{
checkArrows(dailyReset);
}
}
}
private void checkHerbBoxes(boolean dailyReset)
{
if (client.getAccountType() == AccountType.NORMAL
&& client.getVar(VarPlayer.NMZ_REWARD_POINTS) >= HERB_BOX_COST
&& (client.getVar(Varbits.DAILY_HERB_BOXES_COLLECTED) < HERB_BOX_MAX
|| dailyReset))
{
sendChatMessage(HERB_BOX_MESSAGE);
}
}
private void checkStaves(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_VARROCK_EASY) == 1
&& (client.getVar(Varbits.DAILY_STAVES_COLLECTED) == 0
|| dailyReset))
{
sendChatMessage(STAVES_MESSAGE);
}
}
private void checkEssence(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_ARDOUGNE_MEDIUM) == 1
&& (client.getVar(Varbits.DAILY_ESSENCE_COLLECTED) == 0
|| dailyReset))
{
sendChatMessage(ESSENCE_MESSAGE);
}
}
private void checkRunes(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_WILDERNESS_EASY) == 1
&& (client.getVar(Varbits.DAILY_RUNES_COLLECTED) == 0
|| dailyReset))
{
sendChatMessage(RUNES_MESSAGE);
}
}
private void checkSand(boolean dailyReset)
{
if (client.getVar(Varbits.QUEST_THE_HAND_IN_THE_SAND) >= SAND_QUEST_COMPLETE
&& (client.getVar(Varbits.DAILY_SAND_COLLECTED) == 0
|| dailyReset))
{
sendChatMessage(SAND_MESSAGE);
}
}
private void checkFlax(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_KANDARIN_EASY) == 1
&& (client.getVar(Varbits.DAILY_FLAX_STATE) == 0
|| dailyReset))
{
sendChatMessage(FLAX_MESSAGE);
}
}
private void checkArrows(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_WESTERN_EASY) == 1
&& (client.getVar(Varbits.DAILY_ARROWS_STATE) == 0
|| dailyReset))
{
sendChatMessage(ARROWS_MESSAGE);
}
}
private void checkBonemeal(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_MORYTANIA_MEDIUM) == 1)
{
int collected = client.getVar(Varbits.DAILY_BONEMEAL_STATE);
int max = BONEMEAL_PER_DIARY;
if (client.getVar(Varbits.DIARY_MORYTANIA_HARD) == 1)
{
max += BONEMEAL_PER_DIARY;
if (client.getVar(Varbits.DIARY_MORYTANIA_ELITE) == 1)
{
max += BONEMEAL_PER_DIARY;
}
}
if (collected < max || dailyReset)
{
sendChatMessage(BONEMEAL_MESSAGE);
}
}
}
private void checkDynamite(boolean dailyReset)
{
if (client.getVar(Varbits.DIARY_KOUREND_MEDIUM) == 1
&& (client.getVar(Varbits.DAILY_DYNAMITE_COLLECTED) == 0
|| dailyReset))
{
sendChatMessage(DYNAMITE_MESSAGE);
}
}
private void sendChatMessage(String chatMessage)
{
final String message = new ChatMessageBuilder()
.append(ChatColorType.HIGHLIGHT)
.append(chatMessage)
.build();
chatMessageManager.queue(
QueuedMessage.builder()
.type(ChatMessageType.CONSOLE)
.runeLiteFormattedMessage(message)
.build());
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.defaultworld;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup(DefaultWorldConfig.GROUP)
public interface DefaultWorldConfig extends Config
{
String GROUP = "defaultworld";
@ConfigItem(
keyName = "defaultWorld",
name = "Default world",
description = "World to use as default one"
)
default int getWorld()
{
return 0;
}
@ConfigItem(
keyName = "useLastWorld",
name = "Use Last World",
description = "Use the last world you used as the default"
)
default boolean useLastWorld()
{
return false;
}
@ConfigItem(
keyName = "lastWorld",
name = "",
description = "",
hidden = true
)
default int lastWorld()
{
return 0;
}
@ConfigItem(
keyName = "lastWorld",
name = "",
description = ""
)
void lastWorld(int lastWorld);
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.defaultworld;
import com.google.inject.Provides;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.events.GameStateChanged;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.SessionOpen;
import net.runelite.client.game.WorldService;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.util.WorldUtil;
import net.runelite.http.api.worlds.World;
import net.runelite.http.api.worlds.WorldResult;
@PluginDescriptor(
name = "Default World",
description = "Enable a default world to be selected when launching the client",
tags = {"home"}
)
@Slf4j
public class DefaultWorldPlugin extends Plugin
{
@Inject
private Client client;
@Inject
private DefaultWorldConfig config;
@Inject
private WorldService worldService;
private int worldCache;
private boolean worldChangeRequired;
@Override
protected void startUp() throws Exception
{
worldChangeRequired = true;
applyWorld();
}
@Override
protected void shutDown() throws Exception
{
worldChangeRequired = true;
changeWorld(worldCache);
}
@Provides
DefaultWorldConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(DefaultWorldConfig.class);
}
@Subscribe
public void onSessionOpen(SessionOpen event)
{
worldChangeRequired = true;
applyWorld();
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState() == GameState.LOGGED_IN)
{
config.lastWorld(client.getWorld());
}
applyWorld();
}
private void changeWorld(int newWorld)
{
if (!worldChangeRequired || client.getGameState() != GameState.LOGIN_SCREEN)
{
return;
}
worldChangeRequired = false;
int correctedWorld = newWorld < 300 ? newWorld + 300 : newWorld;
// Old School RuneScape worlds start on 301 so don't even bother trying to find lower id ones
// and also do not try to set world if we are already on it
if (correctedWorld <= 300 || client.getWorld() == correctedWorld)
{
return;
}
final WorldResult worldResult = worldService.getWorlds();
if (worldResult == null)
{
log.warn("Failed to lookup worlds.");
return;
}
final World world = worldResult.findWorld(correctedWorld);
if (world != null)
{
final net.runelite.api.World rsWorld = client.createWorld();
rsWorld.setActivity(world.getActivity());
rsWorld.setAddress(world.getAddress());
rsWorld.setId(world.getId());
rsWorld.setPlayerCount(world.getPlayers());
rsWorld.setLocation(world.getLocation());
rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes()));
client.changeWorld(rsWorld);
log.debug("Applied new world {}", correctedWorld);
}
else
{
log.warn("World {} not found.", correctedWorld);
}
}
private void applyWorld()
{
if (worldCache == 0)
{
worldCache = client.getWorld();
log.debug("Stored old world {}", worldCache);
}
final int newWorld = !config.useLastWorld() ? config.getWorld() : config.lastWorld();
changeWorld(newWorld);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, PandahRS <https://github.com/PandahRS>
* 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.discord;
enum DiscordAreaType
{
BOSSES,
CITIES,
DUNGEONS,
MINIGAMES,
RAIDS,
REGIONS
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.discord;
import lombok.AllArgsConstructor;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Units;
@ConfigGroup("discord")
public interface DiscordConfig extends Config
{
@AllArgsConstructor
enum ElapsedTimeType
{
TOTAL("Total elapsed time"),
ACTIVITY("Per activity"),
HIDDEN("Hide elapsed time");
private final String value;
@Override
public String toString()
{
return value;
}
}
@ConfigItem(
keyName = "elapsedTime",
name = "Elapsed Time",
description = "Configures elapsed time shown.",
position = 1
)
default ElapsedTimeType elapsedTimeType()
{
return ElapsedTimeType.ACTIVITY;
}
@ConfigItem(
keyName = "actionTimeout",
name = "Activity timeout",
description = "Configures after how long of not updating activity will be reset (in minutes)",
position = 2
)
@Units(Units.MINUTES)
default int actionTimeout()
{
return 5;
}
@ConfigItem(
keyName = "showSkillActivity",
name = "Skilling",
description = "Show your activity while training skills",
position = 3
)
default boolean showSkillingActivity()
{
return true;
}
@ConfigItem(
keyName = "showBossActivity",
name = "Bosses",
description = "Show your activity and location while at bosses",
position = 4
)
default boolean showBossActivity()
{
return true;
}
@ConfigItem(
keyName = "showCityActivity",
name = "Cities",
description = "Show your activity and location while in cities",
position = 5
)
default boolean showCityActivity()
{
return true;
}
@ConfigItem(
keyName = "showDungeonActivity",
name = "Dungeons",
description = "Show your activity and location while in dungeons",
position = 6
)
default boolean showDungeonActivity()
{
return true;
}
@ConfigItem(
keyName = "showMinigameActivity",
name = "Minigames",
description = "Show your activity and location while in minigames",
position = 7
)
default boolean showMinigameActivity()
{
return true;
}
@ConfigItem(
keyName = "showRaidingActivity",
name = "Raids",
description = "Show your activity and location while in Raids",
position = 8
)
default boolean showRaidingActivity()
{
return true;
}
@ConfigItem(
keyName = "showRegionsActivity",
name = "Regions",
description = "Show your activity and location while in other regions",
position = 9
)
default boolean showRegionsActivity()
{
return true;
}
}

View File

@@ -0,0 +1,624 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2018, PandahRS <https://github.com/PandahRS>
* Copyright (c) 2020, Brooklyn <https://github.com/Broooklyn>
* 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.discord;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.runelite.api.Client;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
@AllArgsConstructor
@Getter
enum DiscordGameEventType
{
IN_MENU("In Menu", -3, true, true, true, false, true),
IN_GAME("In Game", -3, true, false, false, false, true),
PLAYING_DEADMAN("Playing Deadman Mode", -3),
PLAYING_PVP("Playing in a PVP world", -3),
TRAINING_ATTACK(Skill.ATTACK),
TRAINING_DEFENCE(Skill.DEFENCE),
TRAINING_STRENGTH(Skill.STRENGTH),
TRAINING_HITPOINTS(Skill.HITPOINTS, -1),
TRAINING_SLAYER(Skill.SLAYER, 1),
TRAINING_RANGED(Skill.RANGED),
TRAINING_MAGIC(Skill.MAGIC),
TRAINING_PRAYER(Skill.PRAYER),
TRAINING_COOKING(Skill.COOKING),
TRAINING_WOODCUTTING(Skill.WOODCUTTING),
TRAINING_FLETCHING(Skill.FLETCHING),
TRAINING_FISHING(Skill.FISHING, 1),
TRAINING_FIREMAKING(Skill.FIREMAKING),
TRAINING_CRAFTING(Skill.CRAFTING),
TRAINING_SMITHING(Skill.SMITHING),
TRAINING_MINING(Skill.MINING),
TRAINING_HERBLORE(Skill.HERBLORE),
TRAINING_AGILITY(Skill.AGILITY),
TRAINING_THIEVING(Skill.THIEVING),
TRAINING_FARMING(Skill.FARMING),
TRAINING_RUNECRAFT(Skill.RUNECRAFT),
TRAINING_HUNTER(Skill.HUNTER),
TRAINING_CONSTRUCTION(Skill.CONSTRUCTION),
// Bosses
BOSS_ABYSSAL_SIRE("Abyssal Sire", DiscordAreaType.BOSSES, 11851, 11850, 12363, 12362),
BOSS_CERBERUS("Cerberus", DiscordAreaType.BOSSES, 4883, 5140, 5395),
BOSS_COMMANDER_ZILYANA("Commander Zilyana", DiscordAreaType.BOSSES, 11602),
BOSS_CORP("Corporeal Beast", DiscordAreaType.BOSSES, 11842, 11844),
BOSS_DKS("Dagannoth Kings", DiscordAreaType.BOSSES, 11588, 11589),
BOSS_GENERAL_GRAARDOR("General Graardor", DiscordAreaType.BOSSES, 11347),
BOSS_GIANT_MOLE("Giant Mole", DiscordAreaType.BOSSES, 6993, 6992),
BOSS_GROTESQUE_GUARDIANS("Grotesque Guardians", DiscordAreaType.BOSSES, 6727),
BOSS_HESPORI("Hespori", DiscordAreaType.BOSSES, 5021),
BOSS_HYDRA("Alchemical Hydra", DiscordAreaType.BOSSES, 5536),
BOSS_KQ("Kalphite Queen", DiscordAreaType.BOSSES, 13972),
BOSS_KRAKEN("Kraken", DiscordAreaType.BOSSES, 9116),
BOSS_KREEARRA("Kree'arra", DiscordAreaType.BOSSES, 11346),
BOSS_KRIL_TSUTSAROTH("K'ril Tsutsaroth", DiscordAreaType.BOSSES, 11603),
BOSS_NIGHTMARE("Nightmare of Ashihama", DiscordAreaType.BOSSES, 15515),
BOSS_SARACHNIS("Sarachnis", DiscordAreaType.BOSSES, 7322),
BOSS_SKOTIZO("Skotizo", DiscordAreaType.BOSSES, 6810),
BOSS_SMOKE_DEVIL("Thermonuclear smoke devil", DiscordAreaType.BOSSES, 9363, 9619),
BOSS_VORKATH("Vorkath", DiscordAreaType.BOSSES, 9023),
BOSS_WINTERTODT("Wintertodt", DiscordAreaType.BOSSES, 6462),
BOSS_ZALCANO("Zalcano", DiscordAreaType.BOSSES, 12126),
BOSS_ZULRAH("Zulrah", DiscordAreaType.BOSSES, 9007),
// Cities
CITY_AL_KHARID("Al Kharid" , DiscordAreaType.CITIES, 13105, 13106),
CITY_ARCEUUS_HOUSE("Arceuus" , DiscordAreaType.CITIES, 6458, 6459, 6460, 6714, 6715),
CITY_ARDOUGNE("Ardougne" , DiscordAreaType.CITIES, 9779, 9780, 10035, 10036, 10291, 10292, 10547, 10548),
CITY_BANDIT_CAMP("Bandit Camp" , DiscordAreaType.CITIES, 12590),
CITY_BARBARIAN_OUTPOST("Barbarian Outpost", DiscordAreaType.CITIES, 10039),
CITY_BARBARIAN_VILLAGE("Barbarian Village" , DiscordAreaType.CITIES, 12341),
CITY_BEDABIN_CAMP("Bedabin Camp" , DiscordAreaType.CITIES, 12591),
CITY_BRIMHAVEN("Brimhaven" , DiscordAreaType.CITIES, 11057, 11058),
CITY_BURGH_DE_ROTT("Burgh de Rott" , DiscordAreaType.CITIES, 13874, 13873, 14130, 14129),
CITY_BURTHORPE("Burthorpe" , DiscordAreaType.CITIES, 11319, 11575),
CITY_CANIFIS("Canifis" , DiscordAreaType.CITIES, 13878),
CITY_CATHERBY("Catherby" , DiscordAreaType.CITIES, 11317, 11318, 11061),
CITY_CORSAIR_COVE("Corsair Cove" , DiscordAreaType.CITIES, 10028, 10284),
CITY_DARKMEYER("Darkmeyer", DiscordAreaType.CITIES, 14388, 14644),
CITY_DORGESH_KAAN("Dorgesh-Kaan" , DiscordAreaType.CITIES, 10835, 10834),
CITY_DRAYNOR("Draynor" , DiscordAreaType.CITIES, 12338, 12339),
CITY_EDGEVILLE("Edgeville" , DiscordAreaType.CITIES, 12342),
CITY_ENTRANA("Entrana" , DiscordAreaType.CITIES, 11060, 11316),
CITY_ETCETERIA("Etceteria", DiscordAreaType.CITIES, 10300),
CITY_FALADOR("Falador" , DiscordAreaType.CITIES, 11828, 11572, 11827, 12084),
CITY_GUTANOTH("Gu'Tanoth" , DiscordAreaType.CITIES, 10031),
CITY_GWENITH("Gwenith", DiscordAreaType.CITIES, 8757),
CITY_HOSIDIUS_HOUSE("Hosidius" , DiscordAreaType.CITIES, 6710, 6711, 6712, 6455, 6456, 6966, 6967, 6968, 7221, 7223, 7224, 7478, 7479),
CITY_JATIZSO("Jatizso" , DiscordAreaType.CITIES, 9531),
CITY_KELDAGRIM("Keldagrim" , DiscordAreaType.CITIES, 11423, 11422, 11679, 11678),
CITY_LANDS_END("Land's End", DiscordAreaType.CITIES, 5941),
CITY_LLETYA("Lletya" , DiscordAreaType.CITIES, 9265),
CITY_LOVAKENGJ_HOUSE("Lovakengj" , DiscordAreaType.CITIES, 5692, 5691, 5947, 6203, 6202, 5690, 5946),
CITY_LUMBRIDGE("Lumbridge" , DiscordAreaType.CITIES, 12850),
CITY_LUNAR_ISLE("Lunar Isle" , DiscordAreaType.CITIES, 8253, 8252, 8509, 8508),
CITY_MARIM("Marim", DiscordAreaType.REGIONS, 11051),
CITY_MEIYERDITCH("Meiyerditch" , DiscordAreaType.CITIES, 14132, 14387, 14386, 14385),
CITY_MISCELLANIA("Miscellania" , DiscordAreaType.CITIES, 10044),
CITY_MOR_UL_REK("Mor Ul Rek" , DiscordAreaType.CITIES, 9808, 9807, 10064, 10063),
CITY_MORTTON("Mort'ton" , DiscordAreaType.CITIES, 13875),
CITY_MOS_LE_HARMLESS("Mos Le'Harmless" , DiscordAreaType.CITIES, 14638, 14639, 14894, 14895, 15151, 15406, 15407),
CITY_MOUNT_KARUULM("Mount Karuulm", DiscordAreaType.CITIES, 5179, 4923, 5180),
CITY_MOUNTAIN_CAMP("Mountain Camp", DiscordAreaType.CITIES, 11065),
CITY_MYNYDD("Mynydd", DiscordAreaType.CITIES, 8501),
CITY_NARDAH("Nardah" , DiscordAreaType.CITIES, 13613),
CITY_NEITIZNOT("Neitiznot" , DiscordAreaType.CITIES, 9275),
CITY_PISCARILIUS_HOUSE("Port Piscarilius" , DiscordAreaType.CITIES, 6971, 7227, 6970, 7226),
CITY_PISCATORIS("Piscatoris" , DiscordAreaType.CITIES, 9273),
CITY_POLLNIVNEACH("Pollnivneach" , DiscordAreaType.CITIES, 13358),
CITY_PORT_KHAZARD("Port Khazard" , DiscordAreaType.CITIES, 10545),
CITY_PORT_PHASMATYS("Port Phasmatys" , DiscordAreaType.CITIES, 14646),
CITY_PORT_SARIM("Port Sarim" , DiscordAreaType.CITIES, 12081, 12082),
CITY_PRIFDDINAS("Prifddinas", DiscordAreaType.CITIES, 8499, 8500, 8755, 8756, 9011, 9012, 9013, 12894, 12895, 13150, 13151),
CITY_RELLEKKA("Rellekka" , DiscordAreaType.CITIES, 10297, 10553),
CITY_RIMMINGTON("Rimmington" , DiscordAreaType.CITIES, 11826, 11570),
CITY_SEERS_VILLAGE("Seers' Village" , DiscordAreaType.CITIES, 10806),
CITY_SHAYZIEN_HOUSE("Shayzien" , DiscordAreaType.CITIES, 5944, 5943, 6200, 6199, 5686, 5687, 5688, 5689, 5945),
CITY_SHILO_VILLAGE("Shilo Village" , DiscordAreaType.CITIES, 11310),
CITY_SLEPE("Slepe", DiscordAreaType.CITIES, 14643, 14899, 14900, 14901),
CITY_SOPHANEM("Sophanem" , DiscordAreaType.CITIES, 13099),
CITY_TAI_BWO_WANNAI("Tai Bwo Wannai" , DiscordAreaType.CITIES, 11056, 11055),
CITY_TAVERLEY("Taverley" , DiscordAreaType.CITIES, 11574, 11573),
CITY_TREE_GNOME_STRONGHOLD("Tree Gnome Stronghold" , DiscordAreaType.CITIES, 9525, 9526, 9782, 9781),
CITY_TREE_GNOME_VILLAGE("Tree Gnome Village" , DiscordAreaType.CITIES, 10033),
CITY_TROLL_STRONGHOLD("Troll Stronghold" , DiscordAreaType.CITIES, 11321, 11421),
CITY_UZER("Uzer" , DiscordAreaType.CITIES, 13872),
CITY_VARROCK("Varrock" , DiscordAreaType.CITIES, 12596, 12597, 12852, 12853, 12854, 13108, 13109, 13110),
CITY_VER_SINHAZA("Ver Sinhaza", DiscordAreaType.CITIES, 14642),
CITY_VOID_OUTPOST("Void Knights' Outpost", DiscordAreaType.CITIES, 10537),
CITY_WEISS("Weiss", DiscordAreaType.CITIES, 11325, 11581),
CITY_WITCHHAVEN("Witchaven" , DiscordAreaType.CITIES, 10803),
CITY_YANILLE("Yanille" , DiscordAreaType.CITIES, 10288, 10032),
CITY_ZANARIS("Zanaris" , DiscordAreaType.CITIES, 9285, 9541, 9540, 9797),
CITY_ZULANDRA("Zul-Andra" , DiscordAreaType.CITIES, 8495, 8751),
// Dungeons
DUNGEON_ABANDONED_MINE("Abandoned Mine", DiscordAreaType.DUNGEONS, 13718, 11079, 11078, 11077, 10823, 10822, 10821),
DUNGEON_AH_ZA_RHOON("Ah Za Rhoon", DiscordAreaType.DUNGEONS, 11666),
DUNGEON_ANCIENT_CAVERN("Ancient Cavern", DiscordAreaType.DUNGEONS, 6483, 6995),
DUNGEON_APE_ATOLL("Ape Atoll Dungeon", DiscordAreaType.DUNGEONS, 11150, 10894),
DUNGEON_APE_ATOLL_BANANA_PLANTATION("Ape Atoll Banana Plantation", DiscordAreaType.DUNGEONS, 10895),
DUNGEON_ARDY_SEWERS("Ardougne Sewers", DiscordAreaType.DUNGEONS, 10136, 10647),
DUNGEON_ASGARNIAN_ICE_CAVES("Asgarnian Ice Caves", DiscordAreaType.DUNGEONS, 11925, 12181),
DUNGEON_BERVIRIUS_TOMB("Tomb of Bervirius", DiscordAreaType.DUNGEONS, 11154),
DUNGEON_BRIMHAVEN("Brimhaven Dungeon", DiscordAreaType.DUNGEONS, 10901, 10900, 10899, 10645, 10644, 10643),
DUNGEON_BRINE_RAT_CAVERN("Brine Rat Cavern", DiscordAreaType.DUNGEONS, 10910),
DUNGEON_CATACOMBS_OF_KOUREND("Catacombs of Kourend", DiscordAreaType.DUNGEONS, 6557, 6556, 6813, 6812),
DUNGEON_CHAMPIONS_CHALLENGE("Champions' Challenge", DiscordAreaType.DUNGEONS, 12696),
DUNGEON_CHAOS_DRUID_TOWER("Chaos Druid Tower", DiscordAreaType.DUNGEONS, 10392),
DUNGEON_CHASM_OF_FIRE("Chasm of Fire", DiscordAreaType.DUNGEONS, 5789),
DUNGEON_CHASM_OF_TEARS("Chasm of Tears", DiscordAreaType.DUNGEONS, 12948),
DUNGEON_CHINCHOMPA("Chinchompa Hunting Ground", DiscordAreaType.DUNGEONS, 10129),
DUNGEON_CLOCK_TOWER("Clock Tower Basement", DiscordAreaType.DUNGEONS, 10390),
DUNGEON_CORSAIR_COVE("Corsair Cove Dungeon", DiscordAreaType.DUNGEONS, 8076, 8332),
DUNGEON_CRABCLAW_CAVES("Crabclaw Caves", DiscordAreaType.DUNGEONS, 6553, 6809),
DUNGEON_CRANDOR("Crandor Dungeon", DiscordAreaType.DUNGEONS, 11414),
DUNGEON_CRASH_SITE_CAVERN("Crash Site Cavern", DiscordAreaType.DUNGEONS, 8280, 8536),
DUNGEON_DAEYALT_ESSENCE_MINE("Daeyalt Essence Mine", DiscordAreaType.DUNGEONS, 14744),
DUNGEON_DIGSITE("Digsite Dungeon", DiscordAreaType.DUNGEONS, 13464, 13465),
DUNGEON_DORGESHKAAN("Dorgesh-Kaan South Dungeon", DiscordAreaType.DUNGEONS, 10833),
DUNGEON_DORGESHUUN_MINES("Dorgeshuun Mines", DiscordAreaType.DUNGEONS, 12950, 13206),
DUNGEON_DRAYNOR_SEWERS("Draynor Sewers", DiscordAreaType.DUNGEONS, 12439, 12438),
DUNGEON_DWARVEN_MINES("Dwarven Mines", DiscordAreaType.DUNGEONS, 12185, 12184, 12183),
DUNGEON_EAGLES_PEAK("Eagles' Peak Dungeon", DiscordAreaType.DUNGEONS, 8013),
DUNGEON_ECTOFUNTUS("Ectofuntus", DiscordAreaType.DUNGEONS, 14746),
DUNGEON_EDGEVILLE("Edgeville Dungeon", DiscordAreaType.DUNGEONS, 12441, 12442, 12443, 12698),
DUNGEON_ELEMENTAL_WORKSHOP("Elemental Workshop", DiscordAreaType.DUNGEONS, 10906, 7760),
DUNGEON_ENAKHRAS_TEMPLE("Enakhra's Temple", DiscordAreaType.DUNGEONS, 12423),
DUNGEON_EVIL_CHICKENS_LAIR("Evil Chicken's Lair", DiscordAreaType.DUNGEONS, 9796),
DUNGEON_EXPERIMENT_CAVE("Experiment Cave", DiscordAreaType.DUNGEONS, 14235, 13979),
DUNGEON_FEROX_ENCLAVE("Ferox Enclave Dungeon", DiscordAreaType.DUNGEONS, 12700),
DUNGEON_FORTHOS("Forthos Dungeon", DiscordAreaType.DUNGEONS, 7323),
DUNGEON_FREMENNIK_SLAYER("Fremennik Slayer Dungeon", DiscordAreaType.DUNGEONS, 10907, 10908, 11164),
DUNGEON_GLARIALS_TOMB("Glarial's Tomb", DiscordAreaType.DUNGEONS, 10137),
DUNGEON_GOBLIN_CAVE("Goblin Cave", DiscordAreaType.DUNGEONS, 10393),
DUNGEON_GRAND_TREE_TUNNELS("Grand Tree Tunnels", DiscordAreaType.DUNGEONS, 9882),
DUNGEON_HAM_HIDEOUT("H.A.M. Hideout", DiscordAreaType.DUNGEONS, 12694),
DUNGEON_HAM_STORE_ROOM("H.A.M. Store room", DiscordAreaType.DUNGEONS, 10321),
DUNGEON_HEROES_GUILD("Heroes' Guild Mine", DiscordAreaType.DUNGEONS, 11674),
DUNGEON_IORWERTH("Iorwerth Dungeon", DiscordAreaType.DUNGEONS, 12737, 12738, 12993, 12994),
DUNGEON_JATIZSO_MINES("Jatizso Mines", DiscordAreaType.DUNGEONS, 9631),
DUNGEON_JIGGIG_BURIAL_TOMB("Jiggig Burial Tomb", DiscordAreaType.DUNGEONS, 9875, 9874),
DUNGEON_JOGRE("Jogre Dungeon", DiscordAreaType.DUNGEONS, 11412),
DUNGEON_KARAMJA("Karamja Dungeon", DiscordAreaType.DUNGEONS, 11413),
DUNGEON_KARUULM("Karuulm Slayer Dungeon", DiscordAreaType.DUNGEONS, 5280, 5279, 5023, 5535, 5022, 4766, 4510, 4511, 4767, 4768, 4512),
DUNGEON_KGP_HEADQUARTERS("KGP Headquarters", DiscordAreaType.DUNGEONS, 10658),
DUNGEON_KRUK("Kruk's Dungeon", DiscordAreaType.DUNGEONS, 9358, 9359, 9360, 9615, 9616, 9871, 10125, 10126, 10127, 10128, 10381, 10382, 10383, 10384, 10637, 10638, 10639, 10640),
DUNGEON_LEGENDS_GUILD("Legends' Guild Dungeon", DiscordAreaType.DUNGEONS, 10904),
DUNGEON_LIGHTHOUSE("Lighthouse", DiscordAreaType.DUNGEONS, 10140),
DUNGEON_LIZARDMAN_CAVES("Lizardman Caves", DiscordAreaType.DUNGEONS, 5275),
DUNGEON_LIZARDMAN_TEMPLE("Lizardman Temple", DiscordAreaType.DUNGEONS, 5277),
DUNGEON_LUMBRIDGE_SWAMP_CAVES("Lumbridge Swamp Caves", DiscordAreaType.DUNGEONS, 12693, 12949),
DUNGEON_LUNAR_ISLE_MINE("Lunar Isle Mine", DiscordAreaType.DUNGEONS, 9377),
DUNGEON_MANIACAL_HUNTER("Maniacal Monkey Hunter Area", DiscordAreaType.DUNGEONS, 11662),
DUNGEON_MEIYERDITCH_MINE("Meiyerditch Mine", DiscordAreaType.DUNGEONS, 9544),
DUNGEON_MISCELLANIA("Miscellania Dungeon", DiscordAreaType.DUNGEONS, 10144, 10400),
DUNGEON_MOGRE_CAMP("Mogre Camp", DiscordAreaType.DUNGEONS, 11924),
DUNGEON_MOS_LE_HARMLESS_CAVES("Mos Le'Harmless Caves", DiscordAreaType.DUNGEONS, 14994, 14995, 15251),
DUNGEON_MOTHERLODE_MINE("Motherlode Mine", DiscordAreaType.DUNGEONS, 14679, 14680, 14681, 14935, 14936, 14937, 15191, 15192, 15193),
DUNGEON_MOURNER_TUNNELS("Mourner Tunnels", DiscordAreaType.DUNGEONS, 7752, 8008),
DUNGEON_MOUSE_HOLE("Mouse Hole", DiscordAreaType.DUNGEONS, 9046),
DUNGEON_MYREDITCH_LABORATORIES("Myreditch Laboratories", DiscordAreaType.DUNGEONS, 14232, 14233, 14487, 14488),
DUNGEON_MYREQUE("Myreque Hideout", DiscordAreaType.DUNGEONS, 13721, 13974, 13977, 13978),
DUNGEON_MYTHS_GUILD("Myths' Guild Dungeon", DiscordAreaType.DUNGEONS, 7564, 7820, 7821),
DUNGEON_OBSERVATORY("Observatory Dungeon", DiscordAreaType.DUNGEONS, 9362),
DUNGEON_OGRE_ENCLAVE("Ogre Enclave", DiscordAreaType.DUNGEONS, 10387),
DUNGEON_OURANIA("Ourania Cave", DiscordAreaType.DUNGEONS, 12119),
DUNGEON_QUIDAMORTEM_CAVE("Quidamortem Cave", DiscordAreaType.DUNGEONS, 4763),
DUNGEON_RASHILIYIAS_TOMB("Rashiliyta's Tomb", DiscordAreaType.DUNGEONS, 11668),
DUNGEON_SARADOMINSHRINE("Saradomin Shrine (Paterdomus)", DiscordAreaType.DUNGEONS, 13722),
DUNGEON_SHADE_CATACOMBS("Shade Catacombs", DiscordAreaType.DUNGEONS, 13975),
DUNGEON_SHADOW("Shadow Dungeon", DiscordAreaType.DUNGEONS, 10575, 10831),
DUNGEON_SHAYZIEN_CRYPTS("Shayzien Crypts", DiscordAreaType.DUNGEONS, 6043),
DUNGEON_SISTERHOOD_SANCTUARY("Sisterhood Sanctuary", DiscordAreaType.DUNGEONS, 14999, 15000, 15001, 15255, 15256, 15257, 15511, 15512, 15513),
DUNGEON_SMOKE("Smoke Dungeon", DiscordAreaType.DUNGEONS, 12946, 13202),
DUNGEON_SOPHANEM("Sophanem Dungeon", DiscordAreaType.DUNGEONS, 13200),
DUNGEON_SOURHOG_CAVE("Sourhog Cave", DiscordAreaType.DUNGEONS, 12695),
DUNGEON_STRONGHOLD_SECURITY("Stronghold of Security", DiscordAreaType.DUNGEONS, 7505, 8017, 8530, 9297),
DUNGEON_STRONGHOLD_SLAYER("Stronghold Slayer Cave", DiscordAreaType.DUNGEONS, 9624, 9625, 9880, 9881),
DUNGEON_TARNS_LAIR("Tarn's Lair", DiscordAreaType.DUNGEONS, 12616, 12615),
DUNGEON_TAVERLEY("Taverley Dungeon", DiscordAreaType.DUNGEONS, 11416, 11417, 11671, 11672, 11673, 11928, 11929),
DUNGEON_TEMPLE_OF_IKOV("Temple of Ikov", DiscordAreaType.DUNGEONS, 10649, 10905, 10650),
DUNGEON_TEMPLE_OF_LIGHT("Temple of Light", DiscordAreaType.DUNGEONS, 7496),
DUNGEON_TEMPLE_OF_MARIMBO("Temple of Marimbo", DiscordAreaType.DUNGEONS, 11151),
DUNGEON_THE_WARRENS("The Warrens", DiscordAreaType.DUNGEONS, 7070, 7326),
DUNGEON_TOLNA("Dungeon of Tolna", DiscordAreaType.DUNGEONS, 13209),
DUNGEON_TOWER_OF_LIFE("Tower of Life Basement", DiscordAreaType.DUNGEONS, 12100),
DUNGEON_TRAHAEARN_MINE("Trahaearn Mine", DiscordAreaType.DUNGEONS, 13250),
DUNGEON_TUNNEL_OF_CHAOS("Tunnel of Chaos", DiscordAreaType.DUNGEONS, 12625),
DUNGEON_UNDERGROUND_PASS("Underground Pass", DiscordAreaType.DUNGEONS, 9369, 9370),
DUNGEON_VARROCKSEWERS("Varrock Sewers", DiscordAreaType.DUNGEONS, 12954, 13210),
DUNGEON_VIYELDI_CAVES("Viyeldi Caves", DiscordAreaType.DUNGEONS, 9545, 11153),
DUNGEON_WARRIORS_GUILD("Warriors' Guild Basement", DiscordAreaType.DUNGEONS, 11675),
DUNGEON_WATER_RAVINE("Water Ravine", DiscordAreaType.DUNGEONS, 13461),
DUNGEON_WATERBIRTH("Waterbirth Dungeon", DiscordAreaType.DUNGEONS, 9886, 10142, 7492, 7748),
DUNGEON_WATERFALL("Waterfall Dungeon", DiscordAreaType.DUNGEONS, 10394),
DUNGEON_WEREWOLF_AGILITY("Werewolf Agility Course", DiscordAreaType.DUNGEONS, 14234),
DUNGEON_WHITE_WOLF_MOUNTAIN_CAVES("White Wolf Mountain Caves", DiscordAreaType.DUNGEONS, 11418, 11419),
DUNGEON_WITCHAVEN_SHRINE("Witchhaven Shrine Dungeon", DiscordAreaType.DUNGEONS, 10903),
DUNGEON_WIZARDS_TOWER("Wizards' Tower Basement", DiscordAreaType.DUNGEONS, 12437),
DUNGEON_WOODCUTTING_GUILD("Woodcutting Guild Dungeon", DiscordAreaType.DUNGEONS, 6298),
DUNGEON_WYVERN_CAVE("Wyvern Cave", DiscordAreaType.DUNGEONS, 14495, 14496),
DUNGEON_YANILLE_AGILITY("Yanille Agility Dungeon", DiscordAreaType.DUNGEONS, 10388),
// Minigames
MG_ARDOUGNE_RAT_PITS("Ardougne Rat Pits", DiscordAreaType.MINIGAMES, 10646),
MG_BARBARIAN_ASSAULT("Barbarian Assault", DiscordAreaType.MINIGAMES, 7508, 7509, 10322),
MG_BARROWS("Barrows", DiscordAreaType.MINIGAMES, 14131, 14231),
MG_BLAST_FURNACE("Blast Furnace", DiscordAreaType.MINIGAMES, 7757),
MG_BRIMHAVEN_AGILITY_ARENA("Brimhaven Agility Arena", DiscordAreaType.MINIGAMES, 11157),
MG_BURTHORPE_GAMES_ROOM("Burthorpe Games Room", DiscordAreaType.MINIGAMES, 8781),
MG_CASTLE_WARS("Castle Wars", DiscordAreaType.MINIGAMES, 9520, 9620),
MG_CLAN_WARS("Clan Wars", DiscordAreaType.MINIGAMES, 12621, 12622, 12623, 13130, 13131, 13133, 13134, 13135, 13386, 13387, 13390, 13641, 13642, 13643, 13644, 13645, 13646, 13647, 13899, 13900, 14155, 14156),
MG_DUEL_ARENA("Duel Arena", DiscordAreaType.MINIGAMES, 13362, 13363),
MG_FISHING_TRAWLER("Fishing Trawler", DiscordAreaType.MINIGAMES, 7499),
MG_GAUNTLET("The Gauntlet", DiscordAreaType.MINIGAMES, 12127, 7512, 7768),
MG_HALLOWED_SEPULCHRE("Hallowed Sepulchre", DiscordAreaType.MINIGAMES, 8797, 9051, 9052, 9053, 9054, 9309, 9563, 9565, 9821, 10074, 10075, 10077),
MG_INFERNO("The Inferno", DiscordAreaType.MINIGAMES, 9043),
MG_KELDAGRIM_RAT_PITS("Keldagrim Rat Pits", DiscordAreaType.MINIGAMES, 7753),
MG_LAST_MAN_STANDING("Last Man Standing", DiscordAreaType.MINIGAMES, 13660, 13659, 13658, 13916, 13915, 13914),
MG_MAGE_TRAINING_ARENA("Mage Training Arena", DiscordAreaType.MINIGAMES, 13462, 13463),
MG_NIGHTMARE_ZONE("Nightmare Zone", DiscordAreaType.MINIGAMES, 9033),
MG_PEST_CONTROL("Pest Control", DiscordAreaType.MINIGAMES, 10536),
MG_PORT_SARIM_RAT_PITS("Port Sarim Rat Pits", DiscordAreaType.MINIGAMES, 11926),
MG_PYRAMID_PLUNDER("Pyramid Plunder", DiscordAreaType.MINIGAMES, 7749),
MG_ROGUES_DEN("Rogues' Den", DiscordAreaType.MINIGAMES, 11855, 11854, 12111, 12110),
MG_SORCERESS_GARDEN("Sorceress's Garden", DiscordAreaType.MINIGAMES, 11605),
MG_TEMPLE_TREKKING("Temple Trekking", DiscordAreaType.MINIGAMES, 8014, 8270, 8256, 8782, 9038, 9294, 9550, 9806),
MG_TITHE_FARM("Tithe Farm", DiscordAreaType.MINIGAMES, 7222),
MG_TROUBLE_BREWING("Trouble Brewing", DiscordAreaType.MINIGAMES, 15150),
MG_TZHAAR_FIGHT_CAVES("Tzhaar Fight Caves", DiscordAreaType.MINIGAMES, 9551),
MG_TZHAAR_FIGHT_PITS("Tzhaar Fight Pits", DiscordAreaType.MINIGAMES, 9552),
MG_VARROCK_RAT_PITS("Varrock Rat Pits", DiscordAreaType.MINIGAMES, 11599),
MG_VOLCANIC_MINE("Volcanic Mine", DiscordAreaType.MINIGAMES, 15263, 15262),
// Raids
RAIDS_CHAMBERS_OF_XERIC("Chambers of Xeric", DiscordAreaType.RAIDS, Varbits.IN_RAID),
RAIDS_THEATRE_OF_BLOOD("Theatre of Blood", DiscordAreaType.RAIDS, Varbits.THEATRE_OF_BLOOD),
// Other
REGION_ABYSSAL_AREA("Abyssal Area", DiscordAreaType.REGIONS, 12108),
REGION_ABYSSAL_NEXUS("Abyssal Nexus", DiscordAreaType.REGIONS, 12106),
REGION_AGILITY_PYRAMID("Agility Pyramid", DiscordAreaType.REGIONS, 12105, 13356),
REGION_AIR_ALTAR("Air Altar", DiscordAreaType.REGIONS, 11339),
REGION_AL_KHARID_MINE("Al Kharid Mine", DiscordAreaType.REGIONS, 13107),
REGION_APE_ATOLL("Ape Atoll" , DiscordAreaType.REGIONS, 10795, 10974, 11050),
REGION_ARANDAR("Arandar", DiscordAreaType.REGIONS, 9266, 9267, 9523),
REGION_ASGARNIA("Asgarnia", DiscordAreaType.REGIONS, 11825, 11829, 11830, 12085, 12086),
REGION_BATTLEFIELD("Battlefield", DiscordAreaType.REGIONS, 10034),
REGION_BATTLEFRONT("Battlefront", DiscordAreaType.REGIONS, 5433, 5434),
REGION_BLAST_MINE("Blast Mine", DiscordAreaType.REGIONS, 5948),
REGION_BODY_ALTAR("Body Altar", DiscordAreaType.REGIONS, 10059),
REGION_CHAOS_ALTAR("Chaos Altar", DiscordAreaType.REGIONS, 9035),
REGION_COSMIC_ALTAR("Cosmic Altar", DiscordAreaType.REGIONS, 8523),
REGION_COSMIC_ENTITYS_PLANE("Cosmic Entity's Plane", DiscordAreaType.REGIONS, 8267),
REGION_CRABCLAW_ISLE("Crabclaw Isle", DiscordAreaType.REGIONS, 6965),
REGION_CRAFTING_GUILD("Crafting Guild", DiscordAreaType.REGIONS, 11571),
REGION_CRANDOR("Crandor", DiscordAreaType.REGIONS, 11314, 11315),
REGION_CRASH_ISLAND("Crash Island", DiscordAreaType.REGIONS, 11562),
REGION_DARK_ALTAR("Dark Altar", DiscordAreaType.REGIONS, 6716),
REGION_DEATH_ALTAR("Death Altar", DiscordAreaType.REGIONS, 8779),
REGION_DEATH_PLATEAU("Death Plateau", DiscordAreaType.REGIONS, 11320),
REGION_DENSE_ESSENCE("Dense Essence Mine", DiscordAreaType.REGIONS, 6972),
REGION_DIGSITE("Digsite", DiscordAreaType.REGIONS, 13365),
REGION_DRAGONTOOTH("Dragontooth Island", DiscordAreaType.REGIONS, 15159),
REGION_DRAYNOR_MANOR("Draynor Manor", DiscordAreaType.REGIONS, 12340),
REGION_DRILL_SERGEANT("Drill Sergeant's Training Camp", DiscordAreaType.REGIONS, 12619),
REGION_EAGLES_PEAK("Eagles' Peak", DiscordAreaType.REGIONS, 9270),
REGION_EARTH_ALTAR("Earth Altar", DiscordAreaType.REGIONS, 10571),
REGION_ENCHANTED_VALLEY("Enchanted Valley", DiscordAreaType.REGIONS, 12102),
REGION_EVIL_TWIN("Evil Twin Crane Room", DiscordAreaType.REGIONS, 7504),
REGION_EXAM_CENTRE("Exam Centre", DiscordAreaType.REGIONS, 13364),
REGION_FALADOR_FARM("Falador Farm", DiscordAreaType.REGIONS, 12083),
REGION_FARMING_GUILD("Farming Guild", DiscordAreaType.REGIONS, 4922),
REGION_FELDIP_HILLS("Feldip Hills", DiscordAreaType.REGIONS, 9773, 9774, 10029, 10030, 10285, 10286, 10287, 10542, 10543),
REGION_FENKENSTRAIN("Fenkenstrain's Castle", DiscordAreaType.REGIONS, 14135),
REGION_FIRE_ALTAR("Fire Altar", DiscordAreaType.REGIONS, 10315),
REGION_FISHER_REALM("Fisher Realm", DiscordAreaType.REGIONS, 10569),
REGION_FISHING_GUILD("Fishing Guild", DiscordAreaType.REGIONS, 10293),
REGION_FISHING_PLATFORM("Fishing Platform", DiscordAreaType.REGIONS, 11059),
REGION_FORSAKEN_TOWER("The Forsaken Tower", DiscordAreaType.REGIONS, 5435),
REGION_FOSSIL_ISLAND("Fossil Island", DiscordAreaType.REGIONS, 14650, 14651, 14652, 14906, 14907, 14908, 15162, 15163, 15164),
REGION_FREAKY_FORESTER("Freaky Forester's Clearing", DiscordAreaType.REGIONS, 10314),
REGION_FREMENNIK("Fremennik Province", DiscordAreaType.REGIONS, 10296, 10552, 10808, 10809, 10810, 10811, 11064),
REGION_FREMENNIK_ISLES("Fremennik Isles", DiscordAreaType.REGIONS, 9276, 9532),
REGION_FROGLAND("Frogland", DiscordAreaType.REGIONS, 9802),
REGION_GALVEK_SHIPWRECKS("Galvek Shipwrecks", DiscordAreaType.REGIONS, 6486, 6487, 6488, 6489, 6742, 6743, 6744, 6745),
REGION_GORAKS_PLANE("Gorak's Plane", DiscordAreaType.REGIONS, 12115),
REGION_GRAND_EXCHANGE("Grand Exchange", DiscordAreaType.REGIONS, 12598),
REGION_GWD("God Wars Dungeon", DiscordAreaType.REGIONS, 11578),
REGION_HARMONY("Harmony Island", DiscordAreaType.REGIONS, 15148),
REGION_ICE_PATH("Ice Path", DiscordAreaType.REGIONS, 11322, 11323),
REGION_ICEBERG("Iceberg", DiscordAreaType.REGIONS, 10558, 10559),
REGION_ICYENE_GRAVEYARD("Icyene Graveyard", DiscordAreaType.REGIONS, 14641, 14897, 14898),
REGION_ISAFDAR("Isafdar", DiscordAreaType.REGIONS, 8497, 8753, 8754, 9009, 9010),
REGION_ISLAND_OF_STONE("Island of Stone", DiscordAreaType.REGIONS, 9790),
REGION_JIGGIG("Jiggig" , DiscordAreaType.REGIONS, 9775),
REGION_KANDARIN("Kandarin", DiscordAreaType.REGIONS, 9014, 9263, 9264, 9519, 9524, 9527, 9776, 9783, 10037, 10290, 10294, 10546, 10551, 10805),
REGION_KARAMJA("Karamja" , DiscordAreaType.REGIONS, 10801, 10802, 11054, 11311, 11312, 11313, 11566, 11567, 11568, 11569, 11822),
REGION_KEBOS_LOWLANDS("Kebos Lowlands", DiscordAreaType.REGIONS, 4665, 4666, 4921, 5178),
REGION_KEBOS_SWAMP("Kebos Swamp", DiscordAreaType.REGIONS, 4664, 4920, 5174, 5175, 5176, 5430, 5431),
REGION_KHARAZI_JUNGLE("Kharazi Jungle", DiscordAreaType.REGIONS, 11053, 11309, 11565, 11821),
REGION_KHARIDIAN_DESERT("Kharidian Desert", DiscordAreaType.REGIONS, 12844, 12845, 12846, 12847, 12848, 13100, 13101, 13102, 13103, 13104, 13357, 13359, 13360, 13614, 13615, 13616),
REGION_KILLERWATT_PLANE("Killerwatt Plane", DiscordAreaType.REGIONS, 10577),
REGION_KOUREND("Great Kourend", DiscordAreaType.REGIONS, 6201, 6457, 6713),
REGION_KOUREND_WOODLAND("Kourend Woodland", DiscordAreaType.REGIONS, 5942, 6197, 6453),
REGION_LAW_ALTAR("Law Altar", DiscordAreaType.REGIONS, 9803),
REGION_LEGENDS_GUILD("Legends' Guild", DiscordAreaType.REGIONS, 10804),
REGION_LIGHTHOUSE("Lighthouse", DiscordAreaType.REGIONS, 10040),
REGION_LITHKREN("Lithkren", DiscordAreaType.REGIONS, 14142, 14398),
REGION_LUMBRIDGE_SWAMP("Lumbridge Swamp", DiscordAreaType.REGIONS, 12593, 12849),
REGION_MAX_ISLAND("Max Island", DiscordAreaType.REGIONS, 11063),
REGION_MCGRUBORS_WOOD("McGrubor's Wood", DiscordAreaType.REGIONS, 10550),
REGION_MIME_STAGE("Mime's Stage", DiscordAreaType.REGIONS, 8010),
REGION_MIND_ALTAR("Mind Altar", DiscordAreaType.REGIONS, 11083),
REGION_MISTHALIN("Misthalin", DiscordAreaType.REGIONS, 12594, 12595),
REGION_MOLCH("Molch", DiscordAreaType.REGIONS, 5177),
REGION_MOLCH_ISLAND("Molch Island", DiscordAreaType.REGIONS, 5432),
REGION_MORYTANIA("Morytania", DiscordAreaType.REGIONS, 13619, 13620, 13621, 13622, 13876, 13877, 13879, 14133, 14134, 14389, 14390, 14391, 14645, 14647),
REGION_MOUNT_QUIDAMORTEM("Mount Quidamortem", DiscordAreaType.REGIONS, 4662, 4663, 4918, 4919),
REGION_MR_MORDAUTS_CLASSROOM("Mr. Mordaut's Classroom", DiscordAreaType.REGIONS, 7502),
REGION_MUDSKIPPER("Mudskipper Point", DiscordAreaType.REGIONS, 11824),
REGION_MYSTERIOUS_OLD_MAN_MAZE("Mysterious Old Man's Maze", DiscordAreaType.REGIONS, 11590, 11591, 11846, 11847),
REGION_MYTHS_GUILD("Myths' Guild", DiscordAreaType.REGIONS, 9772),
REGION_NATURE_ALTAR("Nature Altar", DiscordAreaType.REGIONS, 9547),
REGION_NORTHERN_TUNDRAS("Northern Tundras", DiscordAreaType.REGIONS, 6204, 6205, 6717),
REGION_OBSERVATORY("Observatory", DiscordAreaType.REGIONS, 9777),
REGION_ODD_ONE_OUT("Odd One Out", DiscordAreaType.REGIONS, 7754),
REGION_OTTOS_GROTTO("Otto's Grotto", DiscordAreaType.REGIONS, 10038),
REGION_OURANIA_HUNTER("Ourania Hunter Area", DiscordAreaType.REGIONS, 9778),
REGION_PIRATES_COVE("Pirates' Cove", DiscordAreaType.REGIONS, 8763),
REGION_PISCATORIS_HUNTER_AREA("Piscatoris Hunter Area", DiscordAreaType.REGIONS, 9015, 9016, 9271, 9272, 9528),
REGION_POH("Player Owned House", DiscordAreaType.REGIONS, 7513, 7514, 7769, 7770),
REGION_POISON_WASTE("Poison Waste", DiscordAreaType.REGIONS, 8752, 9008),
REGION_PORT_TYRAS("Port Tyras", DiscordAreaType.REGIONS, 8496),
REGION_PURO_PURO("Puro Puro", DiscordAreaType.REGIONS, 10307),
REGION_QUARRY("Quarry", DiscordAreaType.REGIONS, 12589),
REGION_RANGING_GUILD("Ranging Guild", DiscordAreaType.REGIONS, 10549),
REGION_RATCATCHERS_MANSION("Ratcatchers Mansion", DiscordAreaType.REGIONS, 11343),
REGION_RUNE_ESSENCE_MINE("Rune Essence Mine", DiscordAreaType.REGIONS, 11595),
// The Beekeper, Pinball, and Gravedigger randoms share a region (7758), and although they are not technically ScapeRune, that name is most commonly
// associated with random events, so those three have been denoted ScapeRune to avoid leaving multiple random event regions without an assigned name.
REGION_SCAPERUNE("ScapeRune", DiscordAreaType.REGIONS, 10058, 7758, 8261),
REGION_SHIP_YARD("Ship Yard", DiscordAreaType.REGIONS, 11823),
REGION_SILVAREA("Silvarea", DiscordAreaType.REGIONS, 13366),
REGION_SINCLAR_MANSION("Sinclair Mansion", DiscordAreaType.REGIONS, 10807),
REGION_SLAYER_TOWER("Slayer Tower", DiscordAreaType.REGIONS, 13623, 13723),
REGION_SOUL_ALTAR("Soul Altar", DiscordAreaType.REGIONS, 7228),
REGION_TROLL_ARENA("Troll Arena", DiscordAreaType.REGIONS, 11576),
REGION_TROLLHEIM("Trollheim", DiscordAreaType.REGIONS, 11577),
REGION_TROLLWEISS_MTN("Trollweiss Mountain", DiscordAreaType.REGIONS, 11066, 11067, 11068),
REGION_UNDERWATER("Underwater", DiscordAreaType.REGIONS, 15008, 15264),
REGION_WATER_ALTAR("Water Altar", DiscordAreaType.REGIONS, 10827),
REGION_WINTERTODT_CAMP("Wintertodt Camp", DiscordAreaType.REGIONS, 6461),
REGION_WIZARDS_TOWER("Wizards' Tower", DiscordAreaType.REGIONS, 12337),
REGION_WOODCUTTING_GUILD("Woodcutting Guild", DiscordAreaType.REGIONS, 6198, 6454),
REGION_WRATH_ALTAR("Wrath Altar", DiscordAreaType.REGIONS, 9291);
private static final Map<Integer, DiscordGameEventType> FROM_REGION;
private static final List<DiscordGameEventType> FROM_VARBITS;
static
{
ImmutableMap.Builder<Integer, DiscordGameEventType> regionMapBuilder = new ImmutableMap.Builder<>();
ImmutableList.Builder<DiscordGameEventType> fromVarbitsBuilder = ImmutableList.builder();
for (DiscordGameEventType discordGameEventType : DiscordGameEventType.values())
{
if (discordGameEventType.getVarbits() != null)
{
fromVarbitsBuilder.add(discordGameEventType);
continue;
}
if (discordGameEventType.getRegionIds() == null)
{
continue;
}
for (int region : discordGameEventType.getRegionIds())
{
regionMapBuilder.put(region, discordGameEventType);
}
}
FROM_REGION = regionMapBuilder.build();
FROM_VARBITS = fromVarbitsBuilder.build();
}
@Nullable
private String imageKey;
@Nullable
private String state;
@Nullable
private String details;
private int priority;
/**
* Marks this event as root event, e.g event that should be used for total time tracking
*/
private boolean root;
/**
* Determines if event should clear other clearable events when triggered
*/
private boolean shouldClear;
/**
* Determines if event should be processed when it timeouts based on action timeout
*/
private boolean shouldTimeout;
/**
* Determines if event start time should be reset when processed
*/
private boolean shouldRestart;
/**
* Determines if event should be cleared when processed
*/
private boolean shouldBeCleared = true;
@Nullable
private DiscordAreaType discordAreaType;
@Nullable
private Varbits varbits;
@Nullable
private int[] regionIds;
DiscordGameEventType(Skill skill)
{
this(skill, 0);
}
DiscordGameEventType(Skill skill, int priority)
{
this.details = training(skill);
this.priority = priority;
this.imageKey = imageKeyOf(skill);
this.shouldTimeout = true;
}
DiscordGameEventType(String areaName, DiscordAreaType areaType, int... regionIds)
{
this.state = exploring(areaType, areaName);
this.priority = -2;
this.discordAreaType = areaType;
this.regionIds = regionIds;
this.shouldClear = true;
}
DiscordGameEventType(String state, int priority, boolean shouldClear, boolean shouldTimeout, boolean shouldRestart, boolean shouldBeCleared, boolean root)
{
this.state = state;
this.priority = priority;
this.shouldClear = shouldClear;
this.shouldTimeout = shouldTimeout;
this.shouldRestart = shouldRestart;
this.shouldBeCleared = shouldBeCleared;
this.root = root;
}
DiscordGameEventType(String state, int priority)
{
this(state, priority, true, false, false, true, false);
}
DiscordGameEventType(String areaName, DiscordAreaType areaType, Varbits varbits)
{
this.state = exploring(areaType, areaName);
this.priority = -2;
this.discordAreaType = areaType;
this.varbits = varbits;
this.shouldClear = true;
}
private static String training(final Skill skill)
{
return training(skill.getName());
}
private static String training(final String what)
{
return "Training: " + what;
}
private static String imageKeyOf(final Skill skill)
{
return imageKeyOf(skill.getName().toLowerCase());
}
private static String imageKeyOf(final String what)
{
return "icon_" + what;
}
private static String exploring(DiscordAreaType areaType, String areaName)
{
return areaName;
}
public static DiscordGameEventType fromSkill(final Skill skill)
{
switch (skill)
{
case ATTACK: return TRAINING_ATTACK;
case DEFENCE: return TRAINING_DEFENCE;
case STRENGTH: return TRAINING_STRENGTH;
case RANGED: return TRAINING_RANGED;
case PRAYER: return TRAINING_PRAYER;
case MAGIC: return TRAINING_MAGIC;
case COOKING: return TRAINING_COOKING;
case WOODCUTTING: return TRAINING_WOODCUTTING;
case FLETCHING: return TRAINING_FLETCHING;
case FISHING: return TRAINING_FISHING;
case FIREMAKING: return TRAINING_FIREMAKING;
case CRAFTING: return TRAINING_CRAFTING;
case SMITHING: return TRAINING_SMITHING;
case MINING: return TRAINING_MINING;
case HERBLORE: return TRAINING_HERBLORE;
case AGILITY: return TRAINING_AGILITY;
case THIEVING: return TRAINING_THIEVING;
case SLAYER: return TRAINING_SLAYER;
case FARMING: return TRAINING_FARMING;
case RUNECRAFT: return TRAINING_RUNECRAFT;
case HUNTER: return TRAINING_HUNTER;
case CONSTRUCTION: return TRAINING_CONSTRUCTION;
default: return null;
}
}
public static DiscordGameEventType fromRegion(final int regionId)
{
return FROM_REGION.get(regionId);
}
public static DiscordGameEventType fromVarbit(final Client client)
{
for (DiscordGameEventType fromVarbit : FROM_VARBITS)
{
if (client.getVar(fromVarbit.getVarbits()) != 0)
{
return fromVarbit;
}
}
return null;
}
}

View File

@@ -0,0 +1,450 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2018, PandahRS <https://github.com/PandahRS>
* 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.discord;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Provides;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.time.temporal.ChronoUnit;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.imageio.ImageIO;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.Skill;
import net.runelite.api.WorldType;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.StatChanged;
import net.runelite.api.events.VarbitChanged;
import net.runelite.client.RuneLiteProperties;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.discord.DiscordService;
import net.runelite.client.discord.events.DiscordJoinGame;
import net.runelite.client.discord.events.DiscordJoinRequest;
import net.runelite.client.discord.events.DiscordReady;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.PartyChanged;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.task.Schedule;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.LinkBrowser;
import net.runelite.client.ws.PartyMember;
import net.runelite.client.ws.PartyService;
import net.runelite.client.ws.WSClient;
import net.runelite.http.api.ws.messages.party.UserJoin;
import net.runelite.http.api.ws.messages.party.UserPart;
import net.runelite.http.api.ws.messages.party.UserSync;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@PluginDescriptor(
name = "Discord",
description = "Show your status and activity in the Discord user panel",
tags = {"action", "activity", "external", "integration", "status"}
)
@Slf4j
public class DiscordPlugin extends Plugin
{
@Inject
private Client client;
@Inject
private DiscordConfig config;
@Inject
private ClientToolbar clientToolbar;
@Inject
private DiscordState discordState;
@Inject
private PartyService partyService;
@Inject
private DiscordService discordService;
@Inject
private WSClient wsClient;
@Inject
private OkHttpClient okHttpClient;
private final Map<Skill, Integer> skillExp = new HashMap<>();
private NavigationButton discordButton;
private boolean loginFlag;
@Provides
private DiscordConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(DiscordConfig.class);
}
@Override
protected void startUp() throws Exception
{
final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "discord.png");
discordButton = NavigationButton.builder()
.tab(false)
.tooltip("Join Discord")
.icon(icon)
.onClick(() -> LinkBrowser.browse(RuneLiteProperties.getDiscordInvite()))
.build();
clientToolbar.addNavigation(discordButton);
resetState();
checkForGameStateUpdate();
checkForAreaUpdate();
if (discordService.getCurrentUser() != null)
{
partyService.setUsername(discordService.getCurrentUser().username + "#" + discordService.getCurrentUser().discriminator);
}
wsClient.registerMessage(DiscordUserInfo.class);
}
@Override
protected void shutDown() throws Exception
{
clientToolbar.removeNavigation(discordButton);
resetState();
partyService.changeParty(null);
wsClient.unregisterMessage(DiscordUserInfo.class);
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
switch (event.getGameState())
{
case LOGIN_SCREEN:
resetState();
checkForGameStateUpdate();
return;
case LOGGING_IN:
loginFlag = true;
break;
case LOGGED_IN:
if (loginFlag)
{
loginFlag = false;
resetState();
checkForGameStateUpdate();
}
break;
}
checkForAreaUpdate();
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equalsIgnoreCase("discord"))
{
resetState();
checkForGameStateUpdate();
checkForAreaUpdate();
}
}
@Subscribe
public void onStatChanged(StatChanged statChanged)
{
final Skill skill = statChanged.getSkill();
final int exp = statChanged.getXp();
final Integer previous = skillExp.put(skill, exp);
if (previous == null || previous >= exp)
{
return;
}
final DiscordGameEventType discordGameEventType = DiscordGameEventType.fromSkill(skill);
if (discordGameEventType != null && config.showSkillingActivity())
{
discordState.triggerEvent(discordGameEventType);
}
}
@Subscribe
public void onVarbitChanged(VarbitChanged event)
{
if (!config.showRaidingActivity())
{
return;
}
final DiscordGameEventType discordGameEventType = DiscordGameEventType.fromVarbit(client);
if (discordGameEventType != null)
{
discordState.triggerEvent(discordGameEventType);
}
}
@Subscribe
public void onDiscordReady(DiscordReady event)
{
partyService.setUsername(event.getUsername() + "#" + event.getDiscriminator());
}
@Subscribe
public void onDiscordJoinRequest(DiscordJoinRequest request)
{
// In order for the "Invite to join" message to work we need to have a valid party in Discord presence.
// We lazily create the party here in order to avoid the (1 of 15) being permanently in the Discord status.
if (!partyService.isInParty())
{
// Change to my party id, which is advertised in the Discord presence secret. This will open the socket,
// send a join, and cause a UserJoin later for me, which will then update the presence and allow the
// "Invite to join" to continue.
partyService.changeParty(partyService.getLocalPartyId());
}
}
@Subscribe
public void onDiscordJoinGame(DiscordJoinGame joinGame)
{
UUID partyId = UUID.fromString(joinGame.getJoinSecret());
partyService.changeParty(partyId);
updatePresence();
}
@Subscribe
public void onDiscordUserInfo(final DiscordUserInfo event)
{
final PartyMember memberById = partyService.getMemberById(event.getMemberId());
if (memberById == null || memberById.getAvatar() != null)
{
return;
}
String url = "https://cdn.discordapp.com/avatars/" + event.getUserId() + "/" + event.getAvatarId() + ".png";
if (Strings.isNullOrEmpty(event.getAvatarId()))
{
final String[] split = memberById.getName().split("#", 2);
if (split.length == 2)
{
int disc = Integer.valueOf(split[1]);
int avatarId = disc % 5;
url = "https://cdn.discordapp.com/embed/avatars/" + avatarId + ".png";
}
}
log.debug("Got user avatar {}", url);
final Request request = new Request.Builder()
.url(url)
.build();
okHttpClient.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e)
{
}
@Override
public void onResponse(Call call, Response response) throws IOException
{
try // NOPMD: UseTryWithResources
{
if (!response.isSuccessful())
{
throw new IOException("Unexpected code " + response);
}
final InputStream inputStream = response.body().byteStream();
final BufferedImage image;
synchronized (ImageIO.class)
{
image = ImageIO.read(inputStream);
}
memberById.setAvatar(image);
}
finally
{
response.close();
}
}
});
}
@Subscribe
public void onUserJoin(final UserJoin event)
{
updatePresence();
}
@Subscribe
public void onUserSync(final UserSync event)
{
final PartyMember localMember = partyService.getLocalMember();
if (localMember != null)
{
if (discordService.getCurrentUser() != null)
{
final DiscordUserInfo userInfo = new DiscordUserInfo(
discordService.getCurrentUser().userId,
discordService.getCurrentUser().avatar);
userInfo.setMemberId(localMember.getMemberId());
wsClient.send(userInfo);
}
}
}
@Subscribe
public void onUserPart(final UserPart event)
{
updatePresence();
}
@Subscribe
public void onPartyChanged(final PartyChanged event)
{
updatePresence();
}
@Schedule(
period = 1,
unit = ChronoUnit.MINUTES
)
public void checkForValidStatus()
{
discordState.checkForTimeout();
}
private void updatePresence()
{
discordState.refresh();
}
private void resetState()
{
discordState.reset();
}
private void checkForGameStateUpdate()
{
discordState.triggerEvent(client.getGameState() == GameState.LOGGED_IN
? DiscordGameEventType.IN_GAME
: DiscordGameEventType.IN_MENU);
}
private void checkForAreaUpdate()
{
if (client.getLocalPlayer() == null)
{
return;
}
final int playerRegionID = WorldPoint.fromLocalInstance(client, client.getLocalPlayer().getLocalLocation()).getRegionID();
if (playerRegionID == 0)
{
return;
}
final EnumSet<WorldType> worldType = client.getWorldType();
if (worldType.contains(WorldType.DEADMAN))
{
discordState.triggerEvent(DiscordGameEventType.PLAYING_DEADMAN);
return;
}
else if (WorldType.isPvpWorld(worldType))
{
discordState.triggerEvent(DiscordGameEventType.PLAYING_PVP);
return;
}
DiscordGameEventType discordGameEventType = DiscordGameEventType.fromRegion(playerRegionID);
// NMZ uses the same region ID as KBD. KBD is always on plane 0 and NMZ is always above plane 0
// Since KBD requires going through the wilderness there is no EventType for it
if (DiscordGameEventType.MG_NIGHTMARE_ZONE == discordGameEventType
&& client.getLocalPlayer().getWorldLocation().getPlane() == 0)
{
discordGameEventType = null;
}
if (discordGameEventType == null)
{
// Unknown region, reset to default in-game
discordState.triggerEvent(DiscordGameEventType.IN_GAME);
return;
}
if (!showArea(discordGameEventType))
{
return;
}
discordState.triggerEvent(discordGameEventType);
}
private boolean showArea(final DiscordGameEventType event)
{
if (event == null)
{
return false;
}
switch (event.getDiscordAreaType())
{
case BOSSES: return config.showBossActivity();
case CITIES: return config.showCityActivity();
case DUNGEONS: return config.showDungeonActivity();
case MINIGAMES: return config.showMinigameActivity();
case REGIONS: return config.showRegionsActivity();
}
return false;
}
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.discord;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ComparisonChain;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javax.inject.Inject;
import lombok.Data;
import net.runelite.client.RuneLiteProperties;
import net.runelite.client.discord.DiscordPresence;
import net.runelite.client.discord.DiscordService;
import net.runelite.client.ws.PartyService;
import static net.runelite.client.ws.PartyService.PARTY_MAX;
/**
* This class contains data about currently active discord state.
*/
class DiscordState
{
@Data
private static class EventWithTime
{
private final DiscordGameEventType type;
private Instant start;
private Instant updated;
}
private final UUID partyId = UUID.randomUUID();
private final List<EventWithTime> events = new ArrayList<>();
private final DiscordService discordService;
private final DiscordConfig config;
private final PartyService party;
private DiscordPresence lastPresence;
@Inject
private DiscordState(final DiscordService discordService, final DiscordConfig config, final PartyService party)
{
this.discordService = discordService;
this.config = config;
this.party = party;
}
/**
* Reset state.
*/
void reset()
{
discordService.clearPresence();
events.clear();
lastPresence = null;
}
/**
* Force refresh discord presence
*/
void refresh()
{
if (lastPresence == null)
{
return;
}
final DiscordPresence.DiscordPresenceBuilder presenceBuilder = DiscordPresence.builder()
.state(lastPresence.getState())
.details(lastPresence.getDetails())
.largeImageText(lastPresence.getLargeImageText())
.startTimestamp(lastPresence.getStartTimestamp())
.smallImageKey(lastPresence.getSmallImageKey())
.partyMax(lastPresence.getPartyMax())
.partySize(party.getMembers().size());
if (!party.isInParty() || party.isPartyOwner())
{
// This is only used to identify the invites on Discord's side. Our party ids are the secret.
presenceBuilder.partyId(partyId.toString());
presenceBuilder.joinSecret(party.getLocalPartyId().toString());
}
discordService.updatePresence(presenceBuilder.build());
}
/**
* Trigger new discord state update.
*
* @param eventType discord event type
*/
void triggerEvent(final DiscordGameEventType eventType)
{
final Optional<EventWithTime> foundEvent = events.stream().filter(e -> e.type == eventType).findFirst();
final EventWithTime event;
if (foundEvent.isPresent())
{
event = foundEvent.get();
}
else
{
event = new EventWithTime(eventType);
event.setStart(Instant.now());
events.add(event);
}
event.setUpdated(Instant.now());
if (event.getType().isShouldClear())
{
events.removeIf(e -> e.getType() != eventType && e.getType().isShouldBeCleared());
}
if (event.getType().isShouldRestart())
{
event.setStart(Instant.now());
}
events.sort((a, b) -> ComparisonChain.start()
.compare(b.getType().getPriority(), a.getType().getPriority())
.compare(b.getUpdated(), a.getUpdated())
.result());
updatePresenceWithLatestEvent();
}
private void updatePresenceWithLatestEvent()
{
if (events.isEmpty())
{
reset();
return;
}
final EventWithTime event = events.get(0);
String imageKey = null;
String state = null;
String details = null;
for (EventWithTime eventWithTime : events)
{
if (imageKey == null)
{
imageKey = eventWithTime.getType().getImageKey();
}
if (details == null)
{
details = eventWithTime.getType().getDetails();
}
if (state == null)
{
state = eventWithTime.getType().getState();
}
if (imageKey != null && details != null && state != null)
{
break;
}
}
// Replace snapshot with + to make tooltip shorter (so it will span only 1 line)
final String versionShortHand = RuneLiteProperties.getVersion().replace("-SNAPSHOT", "+");
final DiscordPresence.DiscordPresenceBuilder presenceBuilder = DiscordPresence.builder()
.state(MoreObjects.firstNonNull(state, ""))
.details(MoreObjects.firstNonNull(details, ""))
.largeImageText(RuneLiteProperties.getTitle() + " v" + versionShortHand)
.smallImageKey(imageKey)
.partyMax(PARTY_MAX)
.partySize(party.getMembers().size());
final Instant startTime;
switch (config.elapsedTimeType())
{
case HIDDEN:
startTime = null;
break;
case TOTAL:
// We are tracking total time spent instead of per activity time so try to find
// root event as this indicates start of tracking and find last updated one
// to determine correct state we are in
startTime = events.stream()
.filter(e -> e.getType().isRoot())
.sorted((a, b) -> b.getUpdated().compareTo(a.getUpdated()))
.map(EventWithTime::getStart)
.findFirst()
.orElse(event.getStart());
break;
case ACTIVITY:
default:
startTime = event.getStart();
break;
}
presenceBuilder.startTimestamp(startTime);
if (!party.isInParty() || party.isPartyOwner())
{
presenceBuilder.partyId(partyId.toString());
presenceBuilder.joinSecret(party.getLocalPartyId().toString());
}
final DiscordPresence presence = presenceBuilder.build();
// This is to reduce amount of RPC calls
if (!presence.equals(lastPresence))
{
lastPresence = presence;
discordService.updatePresence(presence);
}
}
/**
* Check for current state timeout and act upon it.
*/
void checkForTimeout()
{
if (events.isEmpty())
{
return;
}
final Duration actionTimeout = Duration.ofMinutes(config.actionTimeout());
final Instant now = Instant.now();
final AtomicBoolean updatedAny = new AtomicBoolean();
final boolean removedAny = events.removeAll(events.stream()
// Find only events that should time out
.filter(event -> event.getType().isShouldTimeout() && now.isAfter(event.getUpdated().plus(actionTimeout)))
// Reset start times on timed events that should restart
.peek(event ->
{
if (event.getType().isShouldRestart())
{
event.setStart(null);
updatedAny.set(true);
}
})
// Now filter out events that should restart as we do not want to remove them
.filter(event -> !event.getType().isShouldRestart())
.filter(event -> event.getType().isShouldBeCleared())
.collect(Collectors.toList())
);
if (removedAny || updatedAny.get())
{
updatePresenceWithLatestEvent();
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2019, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.discord;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.runelite.http.api.ws.messages.party.PartyMemberMessage;
@Value
@EqualsAndHashCode(callSuper = true)
class DiscordUserInfo extends PartyMemberMessage
{
private final String userId;
private final String avatarId;
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2020 Adam <Adam@sigterm.info>
* 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.dpscounter;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("dpscounter")
public interface DpsConfig extends Config
{
@ConfigItem(
position = 0,
keyName = "showDamage",
name = "Show damage",
description = "Show total damage instead of DPS"
)
default boolean showDamage()
{
return false;
}
@ConfigItem(
position = 1,
keyName = "autopause",
name = "Auto pause",
description = "Pause the DPS tracker when a boss dies"
)
default boolean autopause()
{
return false;
}
@ConfigItem(
position = 2,
keyName = "autoreset",
name = "Auto reset",
description = "Reset the DPS tracker when a boss dies"
)
default boolean autoreset()
{
return false;
}
@ConfigItem(
position = 3,
keyName = "bossDamage",
name = "Only boss damage",
description = "Only count damage done to the boss, and not to other NPCs"
)
default boolean bossDamage()
{
return false;
}
}

View File

@@ -0,0 +1,327 @@
/*
* Copyright (c) 2020 Adam <Adam@sigterm.info>
* 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.dpscounter;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Provides;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Actor;
import net.runelite.api.Client;
import net.runelite.api.Hitsplat;
import net.runelite.api.NPC;
import static net.runelite.api.NpcID.*;
import net.runelite.api.Player;
import net.runelite.api.events.HitsplatApplied;
import net.runelite.api.events.NpcDespawned;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.OverlayMenuClicked;
import net.runelite.client.events.PartyChanged;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.ws.PartyMember;
import net.runelite.client.ws.PartyService;
import net.runelite.client.ws.WSClient;
@PluginDescriptor(
name = "DPS Counter",
description = "Counts damage (per second) by a party",
enabledByDefault = false
)
@Slf4j
public class DpsCounterPlugin extends Plugin
{
private static final ImmutableSet<Integer> BOSSES = ImmutableSet.of(
ABYSSAL_SIRE, ABYSSAL_SIRE_5887, ABYSSAL_SIRE_5888, ABYSSAL_SIRE_5889, ABYSSAL_SIRE_5890, ABYSSAL_SIRE_5891, ABYSSAL_SIRE_5908,
ALCHEMICAL_HYDRA, ALCHEMICAL_HYDRA_8616, ALCHEMICAL_HYDRA_8617, ALCHEMICAL_HYDRA_8618, ALCHEMICAL_HYDRA_8619, ALCHEMICAL_HYDRA_8620, ALCHEMICAL_HYDRA_8621, ALCHEMICAL_HYDRA_8622,
AHRIM_THE_BLIGHTED, DHAROK_THE_WRETCHED, GUTHAN_THE_INFESTED, KARIL_THE_TAINTED, TORAG_THE_CORRUPTED, VERAC_THE_DEFILED,
BRYOPHYTA,
CALLISTO, CALLISTO_6609,
CERBERUS, CERBERUS_5863, CERBERUS_5866,
CHAOS_ELEMENTAL, CHAOS_ELEMENTAL_6505,
CHAOS_FANATIC,
COMMANDER_ZILYANA, COMMANDER_ZILYANA_6493,
CORPOREAL_BEAST,
CRAZY_ARCHAEOLOGIST,
CRYSTALLINE_HUNLLEF, CRYSTALLINE_HUNLLEF_9022, CRYSTALLINE_HUNLLEF_9023, CRYSTALLINE_HUNLLEF_9024,
DAGANNOTH_SUPREME, DAGANNOTH_PRIME, DAGANNOTH_REX, DAGANNOTH_SUPREME_6496, DAGANNOTH_PRIME_6497, DAGANNOTH_REX_6498,
DUSK, DAWN, DUSK_7851, DAWN_7852, DAWN_7853, DUSK_7854, DUSK_7855,
GENERAL_GRAARDOR, GENERAL_GRAARDOR_6494,
GIANT_MOLE, GIANT_MOLE_6499,
HESPORI,
KALPHITE_QUEEN, KALPHITE_QUEEN_963, KALPHITE_QUEEN_965, KALPHITE_QUEEN_4303, KALPHITE_QUEEN_4304, KALPHITE_QUEEN_6500, KALPHITE_QUEEN_6501,
KING_BLACK_DRAGON, KING_BLACK_DRAGON_2642, KING_BLACK_DRAGON_6502,
KRAKEN, KRAKEN_6640, KRAKEN_6656,
KREEARRA, KREEARRA_6492,
KRIL_TSUTSAROTH, KRIL_TSUTSAROTH_6495,
THE_MIMIC, THE_MIMIC_8633,
THE_NIGHTMARE, THE_NIGHTMARE_9426, THE_NIGHTMARE_9427, THE_NIGHTMARE_9428, THE_NIGHTMARE_9429, THE_NIGHTMARE_9430, THE_NIGHTMARE_9431, THE_NIGHTMARE_9432, THE_NIGHTMARE_9433,
OBOR,
SARACHNIS,
SCORPIA,
SKOTIZO,
THERMONUCLEAR_SMOKE_DEVIL,
TZKALZUK,
TZTOKJAD, TZTOKJAD_6506,
VENENATIS, VENENATIS_6610,
VETION, VETION_REBORN,
VORKATH, VORKATH_8058, VORKATH_8059, VORKATH_8060, VORKATH_8061,
ZALCANO, ZALCANO_9050,
ZULRAH, ZULRAH_2043, ZULRAH_2044,
// ToB
THE_MAIDEN_OF_SUGADINTI, THE_MAIDEN_OF_SUGADINTI_8361, THE_MAIDEN_OF_SUGADINTI_8362, THE_MAIDEN_OF_SUGADINTI_8363, THE_MAIDEN_OF_SUGADINTI_8364, THE_MAIDEN_OF_SUGADINTI_8365,
PESTILENT_BLOAT,
NYLOCAS_VASILIAS, NYLOCAS_VASILIAS_8355, NYLOCAS_VASILIAS_8356, NYLOCAS_VASILIAS_8357,
SOTETSEG, SOTETSEG_8388,
XARPUS_8340, XARPUS_8341,
VERZIK_VITUR_8370,
VERZIK_VITUR_8372,
VERZIK_VITUR_8374,
// CoX
TEKTON, TEKTON_7541, TEKTON_7542, TEKTON_ENRAGED, TEKTON_ENRAGED_7544, TEKTON_7545,
VESPULA, VESPULA_7531, VESPULA_7532, ABYSSAL_PORTAL,
VANGUARD, VANGUARD_7526, VANGUARD_7527, VANGUARD_7528, VANGUARD_7529,
GREAT_OLM, GREAT_OLM_LEFT_CLAW, GREAT_OLM_RIGHT_CLAW_7553, GREAT_OLM_7554, GREAT_OLM_LEFT_CLAW_7555,
DEATHLY_RANGER, DEATHLY_MAGE,
MUTTADILE, MUTTADILE_7562, MUTTADILE_7563,
VASA_NISTIRIO, VASA_NISTIRIO_7567,
GUARDIAN, GUARDIAN_7570, GUARDIAN_7571, GUARDIAN_7572,
LIZARDMAN_SHAMAN_7573, LIZARDMAN_SHAMAN_7574,
ICE_DEMON, ICE_DEMON_7585,
SKELETAL_MYSTIC, SKELETAL_MYSTIC_7605, SKELETAL_MYSTIC_7606
);
@Inject
private Client client;
@Inject
private OverlayManager overlayManager;
@Inject
private PartyService partyService;
@Inject
private WSClient wsClient;
@Inject
private DpsOverlay dpsOverlay;
@Inject
private DpsConfig dpsConfig;
@Getter(AccessLevel.PACKAGE)
private final Map<String, DpsMember> members = new ConcurrentHashMap<>();
@Getter(AccessLevel.PACKAGE)
private final DpsMember total = new DpsMember("Total");
@Provides
DpsConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(DpsConfig.class);
}
@Override
protected void startUp()
{
total.reset();
overlayManager.add(dpsOverlay);
wsClient.registerMessage(DpsUpdate.class);
}
@Override
protected void shutDown()
{
wsClient.unregisterMessage(DpsUpdate.class);
overlayManager.remove(dpsOverlay);
members.clear();
}
@Subscribe
public void onPartyChanged(PartyChanged partyChanged)
{
members.clear();
}
@Subscribe
public void onHitsplatApplied(HitsplatApplied hitsplatApplied)
{
Player player = client.getLocalPlayer();
Actor actor = hitsplatApplied.getActor();
if (!(actor instanceof NPC))
{
return;
}
Hitsplat hitsplat = hitsplatApplied.getHitsplat();
if (hitsplat.isMine())
{
final int npcId = ((NPC) actor).getId();
boolean isBoss = BOSSES.contains(npcId);
if (dpsConfig.bossDamage() && !isBoss)
{
return;
}
int hit = hitsplat.getAmount();
// Update local member
PartyMember localMember = partyService.getLocalMember();
// If not in a party, user local player name
final String name = localMember == null ? player.getName() : localMember.getName();
DpsMember dpsMember = members.computeIfAbsent(name, DpsMember::new);
dpsMember.addDamage(hit);
// broadcast damage
if (localMember != null)
{
final DpsUpdate specialCounterUpdate = new DpsUpdate(hit);
specialCounterUpdate.setMemberId(localMember.getMemberId());
wsClient.send(specialCounterUpdate);
}
// apply to total
}
else if (hitsplat.isOthers())
{
final int npcId = ((NPC) actor).getId();
boolean isBoss = BOSSES.contains(npcId);
if ((dpsConfig.bossDamage() || actor != player.getInteracting()) && !isBoss)
{
// only track damage to npcs we are attacking, or is a nearby common boss
return;
}
// apply to total
}
else
{
return;
}
unpause();
total.addDamage(hitsplat.getAmount());
}
@Subscribe
public void onDpsUpdate(DpsUpdate dpsUpdate)
{
if (partyService.getLocalMember().getMemberId().equals(dpsUpdate.getMemberId()))
{
return;
}
String name = partyService.getMemberById(dpsUpdate.getMemberId()).getName();
if (name == null)
{
return;
}
unpause();
DpsMember dpsMember = members.computeIfAbsent(name, DpsMember::new);
dpsMember.addDamage(dpsUpdate.getHit());
}
@Subscribe
public void onOverlayMenuClicked(OverlayMenuClicked event)
{
if (event.getEntry() == DpsOverlay.RESET_ENTRY)
{
members.clear();
total.reset();
}
else if (event.getEntry() == DpsOverlay.UNPAUSE_ENTRY)
{
unpause();
}
else if (event.getEntry() == DpsOverlay.PAUSE_ENTRY)
{
pause();
}
}
@Subscribe
public void onNpcDespawned(NpcDespawned npcDespawned)
{
NPC npc = npcDespawned.getNpc();
if (npc.isDead() && BOSSES.contains(npc.getId()))
{
log.debug("Boss has died!");
if (dpsConfig.autoreset())
{
members.values().forEach(DpsMember::reset);
total.reset();
}
else if (dpsConfig.autopause())
{
pause();
}
}
}
private void pause()
{
if (total.isPaused())
{
return;
}
log.debug("Pausing");
for (DpsMember dpsMember : members.values())
{
dpsMember.pause();
}
total.pause();
dpsOverlay.setPaused(true);
}
private void unpause()
{
if (!total.isPaused())
{
return;
}
log.debug("Unpausing");
for (DpsMember dpsMember : members.values())
{
dpsMember.unpause();
}
total.unpause();
dpsOverlay.setPaused(false);
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2020 Adam <Adam@sigterm.info>
* 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.dpscounter;
import java.time.Duration;
import java.time.Instant;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
class DpsMember
{
private final String name;
private Instant start;
private Instant end;
private int damage;
void addDamage(int amount)
{
if (start == null)
{
start = Instant.now();
}
damage += amount;
}
float getDps()
{
if (start == null)
{
return 0;
}
Instant now = end == null ? Instant.now() : end;
int diff = (int) (now.toEpochMilli() - start.toEpochMilli()) / 1000;
if (diff == 0)
{
return 0;
}
return (float) damage / (float) diff;
}
void pause()
{
end = Instant.now();
}
boolean isPaused()
{
return start == null || end != null;
}
void unpause()
{
if (end == null)
{
return;
}
start = start.plus(Duration.between(end, Instant.now()));
end = null;
}
void reset()
{
damage = 0;
start = end = Instant.now();
}
Duration elapsed()
{
return Duration.between(start, end == null ? Instant.now() : end);
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright (c) 2020 Adam <Adam@sigterm.info>
* 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.dpscounter;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.text.DecimalFormat;
import java.time.Duration;
import java.util.Map;
import javax.inject.Inject;
import net.runelite.api.Client;
import static net.runelite.api.MenuAction.RUNELITE_OVERLAY;
import net.runelite.api.Player;
import net.runelite.client.ui.overlay.OverlayMenuEntry;
import net.runelite.client.ui.overlay.OverlayPanel;
import net.runelite.client.ui.overlay.components.ComponentConstants;
import net.runelite.client.ui.overlay.components.LineComponent;
import net.runelite.client.ui.overlay.components.TitleComponent;
import net.runelite.client.ui.overlay.tooltip.Tooltip;
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
import net.runelite.client.util.QuantityFormatter;
import net.runelite.client.ws.PartyService;
class DpsOverlay extends OverlayPanel
{
private static final DecimalFormat DPS_FORMAT = new DecimalFormat("#0.0");
private static final int PANEL_WIDTH_OFFSET = 10; // assumes 8 for panel component border + 2px between left and right
static final OverlayMenuEntry RESET_ENTRY = new OverlayMenuEntry(RUNELITE_OVERLAY, "Reset", "DPS counter");
static final OverlayMenuEntry PAUSE_ENTRY = new OverlayMenuEntry(RUNELITE_OVERLAY, "Pause", "DPS counter");
static final OverlayMenuEntry UNPAUSE_ENTRY = new OverlayMenuEntry(RUNELITE_OVERLAY, "Unpause", "DPS counter");
private final DpsCounterPlugin dpsCounterPlugin;
private final DpsConfig dpsConfig;
private final PartyService partyService;
private final Client client;
private final TooltipManager tooltipManager;
@Inject
DpsOverlay(DpsCounterPlugin dpsCounterPlugin, DpsConfig dpsConfig, PartyService partyService, Client client,
TooltipManager tooltipManager)
{
super(dpsCounterPlugin);
this.dpsCounterPlugin = dpsCounterPlugin;
this.dpsConfig = dpsConfig;
this.partyService = partyService;
this.client = client;
this.tooltipManager = tooltipManager;
getMenuEntries().add(RESET_ENTRY);
setPaused(false);
}
@Override
public void onMouseOver()
{
DpsMember total = dpsCounterPlugin.getTotal();
Duration elapsed = total.elapsed();
long s = elapsed.getSeconds();
String format;
if (s >= 3600)
{
format = String.format("%d:%02d:%02d", s / 3600, (s % 3600) / 60, (s % 60));
}
else
{
format = String.format("%d:%02d", s / 60, (s % 60));
}
tooltipManager.add(new Tooltip("Elapsed time: " + format));
}
@Override
public Dimension render(Graphics2D graphics)
{
Map<String, DpsMember> dpsMembers = dpsCounterPlugin.getMembers();
if (dpsMembers.isEmpty())
{
return null;
}
boolean inParty = !partyService.getMembers().isEmpty();
boolean showDamage = dpsConfig.showDamage();
DpsMember total = dpsCounterPlugin.getTotal();
boolean paused = total.isPaused();
final String title = (inParty ? "Party " : "") + (showDamage ? "Damage" : "DPS") + (paused ? " (paused)" : "");
panelComponent.getChildren().add(
TitleComponent.builder()
.text(title)
.build());
int maxWidth = ComponentConstants.STANDARD_WIDTH;
FontMetrics fontMetrics = graphics.getFontMetrics();
for (DpsMember dpsMember : dpsMembers.values())
{
String left = dpsMember.getName();
String right = showDamage ? QuantityFormatter.formatNumber(dpsMember.getDamage()) : DPS_FORMAT.format(dpsMember.getDps());
maxWidth = Math.max(maxWidth, fontMetrics.stringWidth(left) + fontMetrics.stringWidth(right));
panelComponent.getChildren().add(
LineComponent.builder()
.left(left)
.right(right)
.build());
}
panelComponent.setPreferredSize(new Dimension(maxWidth + PANEL_WIDTH_OFFSET, 0));
if (!inParty)
{
Player player = client.getLocalPlayer();
if (player.getName() != null)
{
DpsMember self = dpsMembers.get(player.getName());
if (self != null && total.getDamage() > self.getDamage())
{
panelComponent.getChildren().add(
LineComponent.builder()
.left(total.getName())
.right(showDamage ? Integer.toString(total.getDamage()) : DPS_FORMAT.format(total.getDps()))
.build());
}
}
}
return super.render(graphics);
}
void setPaused(boolean paused)
{
OverlayMenuEntry remove = paused ? PAUSE_ENTRY : UNPAUSE_ENTRY;
OverlayMenuEntry add = paused ? UNPAUSE_ENTRY : PAUSE_ENTRY;
getMenuEntries().remove(remove);
if (!getMenuEntries().contains(add))
{
getMenuEntries().add(add);
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 Adam <Adam@sigterm.info>
* 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.dpscounter;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.runelite.http.api.ws.messages.party.PartyMemberMessage;
@Value
@EqualsAndHashCode(callSuper = true)
public class DpsUpdate extends PartyMemberMessage
{
private int hit;
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2020, dekvall <https://github.com/dekvall>
* 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 HOLDER 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.driftnet;
import java.util.Set;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.runelite.api.GameObject;
import net.runelite.api.Varbits;
import net.runelite.api.coords.WorldPoint;
@Data
@RequiredArgsConstructor
class DriftNet
{
private final int objectId;
private final Varbits statusVarbit;
private final Varbits countVarbit;
private final Set<WorldPoint> adjacentTiles;
private GameObject net;
private DriftNetStatus status;
private int count;
@Setter
private DriftNetStatus prevTickStatus;
// Nets that are not accepting fish are those currently not accepting, or those which were not
// accepting in the previous tick. (When a fish shoal is 2 tiles adjacent to a drift net and is
// moving to a net that is just being setup it will be denied even though the net is currently
// in the CATCHING status)
boolean isNotAcceptingFish()
{
return (status != DriftNetStatus.CATCH && status != DriftNetStatus.SET) ||
(prevTickStatus != DriftNetStatus.CATCH && prevTickStatus != DriftNetStatus.SET);
}
String getFormattedCountText()
{
return status != DriftNetStatus.UNSET ? count + "/10" : "";
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (c) 2020, dekvall <https://github.com/dekvall>
* 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 HOLDER 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.driftnet;
import java.awt.Color;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Range;
import net.runelite.client.config.Units;
@ConfigGroup(DriftNetPlugin.CONFIG_GROUP)
public interface DriftNetConfig extends Config
{
@ConfigItem(
position = 1,
keyName = "showNetStatus",
name = "Show net status",
description = "Show net status and fish count"
)
default boolean showNetStatus()
{
return true;
}
@ConfigItem(
position = 2,
keyName = "countColor",
name = "Fish count color",
description = "Color of the fish count text"
)
default Color countColor()
{
return Color.WHITE;
}
@ConfigItem(
position = 3,
keyName = "highlightUntaggedFish",
name = "Highlight untagged fish",
description = "Highlight the untagged fish"
)
default boolean highlightUntaggedFish()
{
return true;
}
@ConfigItem(
position = 4,
keyName = "timeoutDelay",
name = "Tagged timeout",
description = "Time required for a tag to expire"
)
@Range(
min = 1,
max = 100
)
@Units(Units.TICKS)
default int timeoutDelay()
{
return 60;
}
@ConfigItem(
keyName = "untaggedFishColor",
name = "Untagged fish color",
description = "Color of untagged fish",
position = 5
)
default Color untaggedFishColor()
{
return Color.CYAN;
}
@ConfigItem(
keyName = "tagAnnette",
name = "Tag Annette when no nets in inventory",
description = "Tag Annette when no nets in inventory",
position = 6
)
default boolean tagAnnetteWhenNoNets()
{
return true;
}
@ConfigItem(
keyName = "annetteTagColor",
name = "Annette tag color",
description = "Color of Annette tag",
position = 7
)
default Color annetteTagColor()
{
return Color.RED;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) 2020, dekvall <https://github.com/dekvall>
* 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.driftnet;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
import javax.inject.Inject;
import net.runelite.api.GameObject;
import net.runelite.api.NPC;
import net.runelite.api.Point;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayPriority;
import net.runelite.client.ui.overlay.OverlayUtil;
class DriftNetOverlay extends Overlay
{
private final DriftNetConfig config;
private final DriftNetPlugin plugin;
@Inject
private DriftNetOverlay(DriftNetConfig config, DriftNetPlugin plugin)
{
this.config = config;
this.plugin = plugin;
setPosition(OverlayPosition.DYNAMIC);
setPriority(OverlayPriority.LOW);
setLayer(OverlayLayer.ABOVE_SCENE);
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!plugin.isInDriftNetArea())
{
return null;
}
if (config.highlightUntaggedFish())
{
renderFish(graphics);
}
if (config.showNetStatus())
{
renderNets(graphics);
}
if (config.tagAnnetteWhenNoNets())
{
renderAnnette(graphics);
}
return null;
}
private void renderFish(Graphics2D graphics)
{
for (NPC fish : plugin.getFish())
{
if (!plugin.getTaggedFish().containsKey(fish))
{
OverlayUtil.renderActorOverlay(graphics, fish, "", config.untaggedFishColor());
}
}
}
private void renderNets(Graphics2D graphics)
{
for (DriftNet net : plugin.getNETS())
{
final Shape polygon = net.getNet().getConvexHull();
if (polygon != null)
{
OverlayUtil.renderPolygon(graphics, polygon, net.getStatus().getColor());
}
String text = net.getFormattedCountText();
Point textLocation = net.getNet().getCanvasTextLocation(graphics, text, 0);
if (textLocation != null)
{
OverlayUtil.renderTextLocation(graphics, textLocation, text, config.countColor());
}
}
}
private void renderAnnette(Graphics2D graphics)
{
GameObject annette = plugin.getAnnette();
if (annette != null && !plugin.isDriftNetsInInventory())
{
OverlayUtil.renderPolygon(graphics, annette.getConvexHull(), config.annetteTagColor());
}
}
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright (c) 2020, dekvall <https://github.com/dekvall>
* 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 HOLDER 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.driftnet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Provides;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject;
import lombok.Getter;
import net.runelite.api.Actor;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.GameState;
import net.runelite.api.InventoryID;
import net.runelite.api.ItemContainer;
import net.runelite.api.ItemID;
import net.runelite.api.NPC;
import net.runelite.api.NpcID;
import net.runelite.api.NullObjectID;
import net.runelite.api.ObjectID;
import net.runelite.api.Player;
import net.runelite.api.Varbits;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameObjectDespawned;
import net.runelite.api.events.GameObjectSpawned;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.InteractingChanged;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.api.events.NpcDespawned;
import net.runelite.api.events.NpcSpawned;
import net.runelite.api.events.VarbitChanged;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
@PluginDescriptor(
name = "Drift Net",
description = "Display information about drift nets",
tags = {"hunter", "fishing", "drift", "net"},
enabledByDefault = false
)
public class DriftNetPlugin extends Plugin
{
static final String CONFIG_GROUP = "driftnet";
private static final int UNDERWATER_REGION = 15008;
private static final String CHAT_PRODDING_FISH = "You prod at the shoal of fish to scare it.";
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private DriftNetConfig config;
@Inject
private OverlayManager overlayManager;
@Inject
private DriftNetOverlay overlay;
@Getter
private Set<NPC> fish = new HashSet<>();
@Getter
private Map<NPC, Integer> taggedFish = new HashMap<>();
@Getter
private final List<DriftNet> NETS = ImmutableList.of(
new DriftNet(NullObjectID.NULL_31433, Varbits.NORTH_NET_STATUS, Varbits.NORTH_NET_CATCH_COUNT, ImmutableSet.of(
new WorldPoint(3746, 10297, 1),
new WorldPoint(3747, 10297, 1),
new WorldPoint(3748, 10297, 1),
new WorldPoint(3749, 10297, 1)
)),
new DriftNet(NullObjectID.NULL_31434, Varbits.SOUTH_NET_STATUS, Varbits.SOUTH_NET_CATCH_COUNT, ImmutableSet.of(
new WorldPoint(3742, 10288, 1),
new WorldPoint(3742, 10289, 1),
new WorldPoint(3742, 10290, 1),
new WorldPoint(3742, 10291, 1),
new WorldPoint(3742, 10292, 1)
)));
@Getter
private boolean inDriftNetArea;
private boolean armInteraction;
@Getter
private boolean driftNetsInInventory;
@Getter
private GameObject annette;
@Provides
DriftNetConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(DriftNetConfig.class);
}
@Override
protected void startUp()
{
overlayManager.add(overlay);
if (client.getGameState() == GameState.LOGGED_IN)
{
clientThread.invokeLater(() ->
{
inDriftNetArea = checkArea();
updateDriftNetVarbits();
});
}
}
@Override
protected void shutDown()
{
overlayManager.remove(overlay);
reset();
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState() != GameState.LOGGED_IN)
{
annette = null;
}
switch (event.getGameState())
{
case LOGIN_SCREEN:
case HOPPING:
case LOADING:
reset();
break;
case LOGGED_IN:
inDriftNetArea = checkArea();
updateDriftNetVarbits();
break;
}
}
private void reset()
{
fish.clear();
taggedFish.clear();
armInteraction = false;
inDriftNetArea = false;
}
@Subscribe
public void onVarbitChanged(VarbitChanged event)
{
updateDriftNetVarbits();
}
private void updateDriftNetVarbits()
{
if (!inDriftNetArea)
{
return;
}
for (DriftNet net : NETS)
{
DriftNetStatus status = DriftNetStatus.of(client.getVar(net.getStatusVarbit()));
int count = client.getVar(net.getCountVarbit());
net.setStatus(status);
net.setCount(count);
}
}
@Subscribe
public void onInteractingChanged(InteractingChanged event)
{
if (armInteraction
&& event.getSource() == client.getLocalPlayer()
&& event.getTarget() instanceof NPC
&& ((NPC) event.getTarget()).getId() == NpcID.FISH_SHOAL)
{
tagFish(event.getTarget());
armInteraction = false;
}
}
private boolean isFishNextToNet(NPC fish, Collection<DriftNet> nets)
{
final WorldPoint fishTile = WorldPoint.fromLocalInstance(client, fish.getLocalLocation());
return nets.stream().anyMatch(net -> net.getAdjacentTiles().contains(fishTile));
}
private boolean isTagExpired(Integer tick)
{
return tick + config.timeoutDelay() < client.getTickCount();
}
@Subscribe
public void onGameTick(GameTick tick)
{
if (!inDriftNetArea)
{
return;
}
List<DriftNet> closedNets = NETS.stream()
.filter(DriftNet::isNotAcceptingFish)
.collect(Collectors.toList());
taggedFish.entrySet().removeIf(entry ->
isTagExpired(entry.getValue()) ||
isFishNextToNet(entry.getKey(), closedNets)
);
NETS.forEach(net -> net.setPrevTickStatus(net.getStatus()));
armInteraction = false;
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
if (!inDriftNetArea)
{
return;
}
if (event.getType() == ChatMessageType.SPAM && event.getMessage().equals(CHAT_PRODDING_FISH))
{
Actor target = client.getLocalPlayer().getInteracting();
if (target instanceof NPC && ((NPC) target).getId() == NpcID.FISH_SHOAL)
{
tagFish(target);
}
else
{
// If the fish is on an adjacent tile, the interaction change happens after
// the chat message is sent, so we arm it
armInteraction = true;
}
}
}
private void tagFish(Actor fish)
{
NPC fishTarget = (NPC) fish;
taggedFish.put(fishTarget, client.getTickCount());
}
@Subscribe
public void onNpcSpawned(NpcSpawned event)
{
final NPC npc = event.getNpc();
if (npc.getId() == NpcID.FISH_SHOAL)
{
fish.add(npc);
}
}
@Subscribe
public void onNpcDespawned(NpcDespawned event)
{
final NPC npc = event.getNpc();
fish.remove(npc);
taggedFish.remove(npc);
}
@Subscribe
public void onGameObjectSpawned(GameObjectSpawned event)
{
GameObject object = event.getGameObject();
if (object.getId() == ObjectID.ANNETTE)
{
annette = object;
}
for (DriftNet net : NETS)
{
if (net.getObjectId() == object.getId())
{
net.setNet(object);
}
}
}
@Subscribe
public void onGameObjectDespawned(GameObjectDespawned event)
{
GameObject object = event.getGameObject();
if (object == annette)
{
annette = null;
}
for (DriftNet net : NETS)
{
if (net.getObjectId() == object.getId())
{
net.setNet(null);
}
}
}
@Subscribe
public void onItemContainerChanged(final ItemContainerChanged event)
{
final ItemContainer itemContainer = event.getItemContainer();
if (itemContainer != client.getItemContainer(InventoryID.INVENTORY))
{
return;
}
driftNetsInInventory = itemContainer.contains(ItemID.DRIFT_NET);
}
private boolean checkArea()
{
final Player localPlayer = client.getLocalPlayer();
if (localPlayer == null || !client.isInInstancedRegion())
{
return false;
}
final WorldPoint point = WorldPoint.fromLocalInstance(client, localPlayer.getLocalLocation());
return point.getRegionID() == UNDERWATER_REGION;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2020, dekvall <https://github.com/dekvall>
* 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 HOLDER 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.driftnet;
import java.awt.Color;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
enum DriftNetStatus
{
UNSET(Color.YELLOW),
SET(Color.GREEN),
CATCH(Color.GREEN),
FULL(Color.RED);
private final Color color;
static DriftNetStatus of(int varbitValue)
{
switch (varbitValue)
{
case 0:
return UNSET;
case 1:
return SET;
case 2:
return CATCH;
case 3:
return FULL;
default:
return null;
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2019, Lotto <https://github.com/devLotto>
* 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.emojis;
import com.google.common.collect.ImmutableMap;
import java.awt.image.BufferedImage;
import java.util.Map;
import net.runelite.client.util.ImageUtil;
enum Emoji
{
SLIGHT_SMILE(":)"),
JOY("=')"),
COWBOY("3:)"),
BLUSH("^_^"),
SMILE(":D"),
GRINNING("=D"),
WINK(";)"),
STUCK_OUT_TONGUE_CLOSED_EYES("X-P"),
STUCK_OUT_TONGUE(":P"),
YUM("=P~"),
HUGGING("<gt>:D<lt>"), // >:D<
TRIUMPH(":<gt>"), // :>
THINKING(":-?"),
CONFUSED(":/"),
NEUTRAL_FACE("=|"),
EXPRESSIONLESS(":|"),
UNAMUSED(":-|"),
SLIGHT_FROWN(":("),
FROWNING2("=("),
CRY(":'("),
SOB(":_("),
FLUSHED(":$"),
ZIPPER_MOUTH(":-#"),
PERSEVERE("<gt>_<lt>"), // >_<
SUNGLASSES("8-)"),
INNOCENT("O:)"),
SMILING_IMP("<gt>:)"), // >:)
RAGE("<gt>:("), // >:(
HUSHED(":-O"),
OPEN_MOUTH(":O"),
SCREAM(":-@"),
SEE_NO_EVIL("X_X"),
DANCER("\\:D/"),
OK_HAND("(Ok)"),
THUMBSUP("(Y)"),
THUMBSDOWN("(N)"),
HEARTS("<lt>3"), // <3
BROKEN_HEART("<lt>/3"), // </3
ZZZ("Zzz"),
FISH("<lt><gt><lt>"), // <><
CAT(":3"),
DOG("=3"),
CRAB("V(;,;)V"),
FORK_AND_KNIFE("--E"),
COOKING("--(o)"),
PARTY_POPPER("@@@"),
EYES("O.O"),
SWEAT(";;"),
PILE_OF_POO("~@~"),
FIRE("(/\\)"),
ALIEN("(@.@)"),
EGGPLANT("8=D"),
WAVE("(^_^)/"),
HEART_EYES("(*.*)"),
FACEPALM("M-)"),
PENSIVE("V_V"),
ACORN("<lt>D~"), // <D~
GORILLA(":G"),
PLEADING("(n_n)"),
XD("Xd"),
;
private static final Map<String, Emoji> emojiMap;
private final String trigger;
static
{
ImmutableMap.Builder<String, Emoji> builder = new ImmutableMap.Builder<>();
for (final Emoji emoji : values())
{
builder.put(emoji.trigger, emoji);
}
emojiMap = builder.build();
}
Emoji(String trigger)
{
this.trigger = trigger;
}
BufferedImage loadImage()
{
return ImageUtil.getResourceStreamFromClass(getClass(), this.name().toLowerCase() + ".png");
}
static Emoji getEmoji(String trigger)
{
return emojiMap.get(trigger);
}
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright (c) 2019, Lotto <https://github.com/devLotto>
* 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.emojis;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.inject.Inject;
import joptsimple.internal.Strings;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.IndexedSprite;
import net.runelite.api.MessageNode;
import net.runelite.api.Player;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.OverheadTextChanged;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.Text;
@PluginDescriptor(
name = "Emojis",
description = "Replaces common emoticons such as :) with their corresponding emoji in the chat",
enabledByDefault = false
)
@Slf4j
public class EmojiPlugin extends Plugin
{
private static final Pattern WHITESPACE_REGEXP = Pattern.compile("[\\s\\u00A0]");
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ChatMessageManager chatMessageManager;
private int modIconsStart = -1;
@Override
protected void startUp()
{
clientThread.invoke(this::loadEmojiIcons);
}
@Subscribe
public void onGameStateChanged(GameStateChanged gameStateChanged)
{
if (gameStateChanged.getGameState() == GameState.LOGGED_IN)
{
loadEmojiIcons();
}
}
private void loadEmojiIcons()
{
final IndexedSprite[] modIcons = client.getModIcons();
if (modIconsStart != -1 || modIcons == null)
{
return;
}
final Emoji[] emojis = Emoji.values();
final IndexedSprite[] newModIcons = Arrays.copyOf(modIcons, modIcons.length + emojis.length);
modIconsStart = modIcons.length;
for (int i = 0; i < emojis.length; i++)
{
final Emoji emoji = emojis[i];
try
{
final BufferedImage image = emoji.loadImage();
final IndexedSprite sprite = ImageUtil.getImageIndexedSprite(image, client);
newModIcons[modIconsStart + i] = sprite;
}
catch (Exception ex)
{
log.warn("Failed to load the sprite for emoji " + emoji, ex);
}
}
log.debug("Adding emoji icons");
client.setModIcons(newModIcons);
}
@Subscribe
public void onChatMessage(ChatMessage chatMessage)
{
if (client.getGameState() != GameState.LOGGED_IN || modIconsStart == -1)
{
return;
}
switch (chatMessage.getType())
{
case PUBLICCHAT:
case MODCHAT:
case FRIENDSCHAT:
case PRIVATECHAT:
case PRIVATECHATOUT:
case MODPRIVATECHAT:
break;
default:
return;
}
final MessageNode messageNode = chatMessage.getMessageNode();
final String message = messageNode.getValue();
final String updatedMessage = updateMessage(message);
if (updatedMessage == null)
{
return;
}
messageNode.setRuneLiteFormatMessage(updatedMessage);
chatMessageManager.update(messageNode);
client.refreshChat();
}
@Subscribe
public void onOverheadTextChanged(final OverheadTextChanged event)
{
if (!(event.getActor() instanceof Player))
{
return;
}
final String message = event.getOverheadText();
final String updatedMessage = updateMessage(message);
if (updatedMessage == null)
{
return;
}
event.getActor().setOverheadText(updatedMessage);
}
@Nullable
String updateMessage(final String message)
{
final String[] messageWords = WHITESPACE_REGEXP.split(message);
boolean editedMessage = false;
for (int i = 0; i < messageWords.length; i++)
{
// Remove tags except for <lt> and <gt>
final String trigger = Text.removeFormattingTags(messageWords[i]);
final Emoji emoji = Emoji.getEmoji(trigger);
if (emoji == null)
{
continue;
}
final int emojiId = modIconsStart + emoji.ordinal();
messageWords[i] = messageWords[i].replace(trigger, "<img=" + emojiId + ">");
editedMessage = true;
}
// If we haven't edited the message any, don't update it.
if (!editedMessage)
{
return null;
}
return Strings.join(messageWords, " ");
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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 HOLDER 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.entityhider;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("entityhider")
public interface EntityHiderConfig extends Config
{
@ConfigItem(
position = 1,
keyName = "hidePlayers",
name = "Hide Players",
description = "Configures whether or not players are hidden"
)
default boolean hidePlayers()
{
return true;
}
@ConfigItem(
position = 2,
keyName = "hidePlayers2D",
name = "Hide Players 2D",
description = "Configures whether or not players 2D elements are hidden"
)
default boolean hidePlayers2D()
{
return true;
}
@ConfigItem(
position = 3,
keyName = "hideFriends",
name = "Hide Friends",
description = "Configures whether or not friends are hidden"
)
default boolean hideFriends()
{
return false;
}
@ConfigItem(
position = 4,
keyName = "hideClanMates",
name = "Hide Friends Chat members",
description = "Configures whether or not friends chat members are hidden"
)
default boolean hideFriendsChatMembers()
{
return false;
}
@ConfigItem(
position = 5,
keyName = "hideLocalPlayer",
name = "Hide Local Player",
description = "Configures whether or not the local player is hidden"
)
default boolean hideLocalPlayer()
{
return false;
}
@ConfigItem(
position = 6,
keyName = "hideLocalPlayer2D",
name = "Hide Local Player 2D",
description = "Configures whether or not the local player's 2D elements are hidden"
)
default boolean hideLocalPlayer2D()
{
return false;
}
@ConfigItem(
position = 7,
keyName = "hideNPCs",
name = "Hide NPCs",
description = "Configures whether or not NPCs are hidden"
)
default boolean hideNPCs()
{
return false;
}
@ConfigItem(
position = 8,
keyName = "hideNPCs2D",
name = "Hide NPCs 2D",
description = "Configures whether or not NPCs 2D elements are hidden"
)
default boolean hideNPCs2D()
{
return false;
}
@ConfigItem(
position = 9,
keyName = "hidePets",
name = "Hide Pets",
description = "Configures whether or not other player pets are hidden"
)
default boolean hidePets()
{
return false;
}
@ConfigItem(
position = 10,
keyName = "hideAttackers",
name = "Hide Attackers",
description = "Configures whether or not NPCs/players attacking you are hidden"
)
default boolean hideAttackers()
{
return false;
}
@ConfigItem(
position = 11,
keyName = "hideProjectiles",
name = "Hide Projectiles",
description = "Configures whether or not projectiles are hidden"
)
default boolean hideProjectiles()
{
return false;
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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 HOLDER 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.entityhider;
import com.google.inject.Provides;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.Player;
import net.runelite.api.coords.WorldPoint;
import net.runelite.client.events.ConfigChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
@PluginDescriptor(
name = "Entity Hider",
description = "Hide players, NPCs, and/or projectiles",
tags = {"npcs", "players", "projectiles"},
enabledByDefault = false
)
public class EntityHiderPlugin extends Plugin
{
@Inject
private Client client;
@Inject
private EntityHiderConfig config;
@Provides
EntityHiderConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(EntityHiderConfig.class);
}
@Override
protected void startUp()
{
updateConfig();
}
@Subscribe
public void onConfigChanged(ConfigChanged e)
{
updateConfig();
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState() == GameState.LOGGED_IN)
{
client.setIsHidingEntities(isPlayerRegionAllowed());
}
}
private void updateConfig()
{
client.setIsHidingEntities(isPlayerRegionAllowed());
client.setPlayersHidden(config.hidePlayers());
client.setPlayersHidden2D(config.hidePlayers2D());
client.setFriendsHidden(config.hideFriends());
client.setFriendsChatMembersHidden(config.hideFriendsChatMembers());
client.setLocalPlayerHidden(config.hideLocalPlayer());
client.setLocalPlayerHidden2D(config.hideLocalPlayer2D());
client.setNPCsHidden(config.hideNPCs());
client.setNPCsHidden2D(config.hideNPCs2D());
client.setPetsHidden(config.hidePets());
client.setAttackersHidden(config.hideAttackers());
client.setProjectilesHidden(config.hideProjectiles());
}
@Override
protected void shutDown() throws Exception
{
client.setIsHidingEntities(false);
client.setPlayersHidden(false);
client.setPlayersHidden2D(false);
client.setFriendsHidden(false);
client.setFriendsChatMembersHidden(false);
client.setLocalPlayerHidden(false);
client.setLocalPlayerHidden2D(false);
client.setNPCsHidden(false);
client.setNPCsHidden2D(false);
client.setPetsHidden(false);
client.setAttackersHidden(false);
client.setProjectilesHidden(false);
}
private boolean isPlayerRegionAllowed()
{
final Player localPlayer = client.getLocalPlayer();
if (localPlayer == null)
{
return true;
}
final int playerRegionID = WorldPoint.fromLocalInstance(client, localPlayer.getLocalLocation()).getRegionID();
// 9520 = Castle Wars
return playerRegionID != 9520;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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.examine;
import java.util.Objects;
class CacheKey
{
private final ExamineType type;
private final int id;
public CacheKey(ExamineType type, int id)
{
this.type = type;
this.id = id;
}
@Override
public int hashCode()
{
int hash = 3;
hash = 23 * hash + Objects.hashCode(this.type);
hash = 23 * hash + this.id;
return hash;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final CacheKey other = (CacheKey) obj;
if (this.id != other.id)
{
return false;
}
return this.type == other.type;
}
}

View File

@@ -0,0 +1,429 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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.examine;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.inject.Provides;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Pattern;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.ItemComposition;
import net.runelite.api.ItemID;
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.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import static net.runelite.api.widgets.WidgetInfo.SEED_VAULT_ITEM_CONTAINER;
import static net.runelite.api.widgets.WidgetInfo.TO_CHILD;
import static net.runelite.api.widgets.WidgetInfo.TO_GROUP;
import net.runelite.api.widgets.WidgetItem;
import net.runelite.client.chat.ChatColorType;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.chat.QueuedMessage;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.util.QuantityFormatter;
import net.runelite.client.util.Text;
import net.runelite.http.api.examine.ExamineClient;
import okhttp3.OkHttpClient;
/**
* Submits examine info to the api
*
* @author Adam
*/
@PluginDescriptor(
name = "Examine",
description = "Send examine information to the API",
tags = {"npcs", "items", "inventory", "objects"}
)
@Slf4j
public class ExaminePlugin extends Plugin
{
private static final Pattern X_PATTERN = Pattern.compile("^\\d+ x ");
private final Deque<PendingExamine> pending = new ArrayDeque<>();
private final Cache<CacheKey, Boolean> cache = CacheBuilder.newBuilder()
.maximumSize(128L)
.build();
@Inject
private ExamineClient examineClient;
@Inject
private Client client;
@Inject
private ItemManager itemManager;
@Inject
private ChatMessageManager chatMessageManager;
@Provides
ExamineClient provideExamineClient(OkHttpClient okHttpClient)
{
return new ExamineClient(okHttpClient);
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
pending.clear();
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked event)
{
if (!Text.removeTags(event.getMenuOption()).equals("Examine"))
{
return;
}
ExamineType type;
int id, quantity = -1;
switch (event.getMenuAction())
{
case EXAMINE_ITEM:
{
type = ExamineType.ITEM;
id = event.getId();
int widgetId = event.getWidgetId();
int widgetGroup = TO_GROUP(widgetId);
int widgetChild = TO_CHILD(widgetId);
Widget widget = client.getWidget(widgetGroup, widgetChild);
WidgetItem widgetItem = widget.getWidgetItem(event.getActionParam());
quantity = widgetItem != null && widgetItem.getId() >= 0 ? widgetItem.getQuantity() : 1;
// Examine on inventory items with more than 100000 quantity is handled locally and shows the item stack
// count, instead of sending the examine packet, so that you can see how many items are in the stack.
// Replace that message with one that formats the quantity using the quantity formatter instead.
if (quantity >= 100_000)
{
int itemId = event.getId();
final ChatMessageBuilder message = new ChatMessageBuilder()
.append(QuantityFormatter.formatNumber(quantity)).append(" x ").append(itemManager.getItemComposition(itemId).getName());
chatMessageManager.queue(QueuedMessage.builder()
.type(ChatMessageType.ITEM_EXAMINE)
.runeLiteFormattedMessage(message.build())
.build());
event.consume();
}
break;
}
case EXAMINE_ITEM_GROUND:
type = ExamineType.ITEM;
id = event.getId();
break;
case CC_OP_LOW_PRIORITY:
{
type = ExamineType.ITEM_BANK_EQ;
int[] qi = findItemFromWidget(event.getWidgetId(), event.getActionParam());
if (qi == null)
{
log.debug("Examine for item with unknown widget: {}", event);
return;
}
quantity = qi[0];
id = qi[1];
break;
}
case EXAMINE_OBJECT:
type = ExamineType.OBJECT;
id = event.getId();
break;
case EXAMINE_NPC:
type = ExamineType.NPC;
id = event.getId();
break;
default:
return;
}
PendingExamine pendingExamine = new PendingExamine();
pendingExamine.setType(type);
pendingExamine.setId(id);
pendingExamine.setQuantity(quantity);
pendingExamine.setCreated(Instant.now());
pending.push(pendingExamine);
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
ExamineType type;
switch (event.getType())
{
case ITEM_EXAMINE:
type = ExamineType.ITEM;
break;
case OBJECT_EXAMINE:
type = ExamineType.OBJECT;
break;
case NPC_EXAMINE:
type = ExamineType.NPC;
break;
case GAMEMESSAGE:
type = ExamineType.ITEM_BANK_EQ;
break;
default:
return;
}
if (pending.isEmpty())
{
log.debug("Got examine without a pending examine?");
return;
}
PendingExamine pendingExamine = pending.pop();
if (pendingExamine.getType() != type)
{
log.debug("Type mismatch for pending examine: {} != {}", pendingExamine.getType(), type);
pending.clear(); // eh
return;
}
log.debug("Got examine for {} {}: {}", pendingExamine.getType(), pendingExamine.getId(), event.getMessage());
// If it is an item, show the price of it
final ItemComposition itemComposition;
if (pendingExamine.getType() == ExamineType.ITEM || pendingExamine.getType() == ExamineType.ITEM_BANK_EQ)
{
final int itemId = pendingExamine.getId();
final int itemQuantity = pendingExamine.getQuantity();
if (itemId == ItemID.COINS_995)
{
return;
}
itemComposition = itemManager.getItemComposition(itemId);
getItemPrice(itemComposition.getId(), itemComposition, itemQuantity);
}
else
{
itemComposition = null;
}
// Don't submit examine info for tradeable items, which we already have from the RS item api
if (itemComposition != null && itemComposition.isTradeable())
{
return;
}
// Large quantities of items show eg. 100000 x Coins
if (type == ExamineType.ITEM && X_PATTERN.matcher(event.getMessage()).lookingAt())
{
return;
}
CacheKey key = new CacheKey(type, pendingExamine.getId());
Boolean cached = cache.getIfPresent(key);
if (cached != null)
{
return;
}
cache.put(key, Boolean.TRUE);
submitExamine(pendingExamine, event.getMessage());
}
private int[] findItemFromWidget(int widgetId, int actionParam)
{
int widgetGroup = TO_GROUP(widgetId);
int widgetChild = TO_CHILD(widgetId);
Widget widget = client.getWidget(widgetGroup, widgetChild);
if (widget == null)
{
return null;
}
if (WidgetInfo.EQUIPMENT.getGroupId() == widgetGroup)
{
Widget widgetItem = widget.getChild(1);
if (widgetItem != null)
{
return new int[]{widgetItem.getItemQuantity(), widgetItem.getItemId()};
}
}
else if (WidgetInfo.SMITHING_INVENTORY_ITEMS_CONTAINER.getGroupId() == widgetGroup)
{
Widget widgetItem = widget.getChild(2);
if (widgetItem != null)
{
return new int[]{widgetItem.getItemQuantity(), widgetItem.getItemId()};
}
}
else if (WidgetInfo.BANK_INVENTORY_ITEMS_CONTAINER.getGroupId() == widgetGroup
|| WidgetInfo.RUNE_POUCH_ITEM_CONTAINER.getGroupId() == widgetGroup)
{
Widget widgetItem = widget.getChild(actionParam);
if (widgetItem != null)
{
return new int[]{widgetItem.getItemQuantity(), widgetItem.getItemId()};
}
}
else if (WidgetInfo.BANK_ITEM_CONTAINER.getGroupId() == widgetGroup
|| WidgetInfo.CLUE_SCROLL_REWARD_ITEM_CONTAINER.getGroupId() == widgetGroup
|| WidgetInfo.LOOTING_BAG_CONTAINER.getGroupId() == widgetGroup
|| WidgetID.SEED_VAULT_INVENTORY_GROUP_ID == widgetGroup
|| WidgetID.SEED_BOX_GROUP_ID == widgetGroup
|| WidgetID.PLAYER_TRADE_SCREEN_GROUP_ID == widgetGroup
|| WidgetID.PLAYER_TRADE_INVENTORY_GROUP_ID == widgetGroup)
{
Widget widgetItem = widget.getChild(actionParam);
if (widgetItem != null)
{
return new int[]{widgetItem.getItemQuantity(), widgetItem.getItemId()};
}
}
else if (WidgetInfo.SHOP_ITEMS_CONTAINER.getGroupId() == widgetGroup)
{
Widget widgetItem = widget.getChild(actionParam);
if (widgetItem != null)
{
return new int[]{1, widgetItem.getItemId()};
}
}
else if (WidgetID.SEED_VAULT_GROUP_ID == widgetGroup)
{
Widget widgetItem = client.getWidget(SEED_VAULT_ITEM_CONTAINER).getChild(actionParam);
if (widgetItem != null)
{
return new int[]{widgetItem.getItemQuantity(), widgetItem.getItemId()};
}
}
return null;
}
@VisibleForTesting
void getItemPrice(int id, ItemComposition itemComposition, int quantity)
{
// quantity is at least 1
quantity = Math.max(1, quantity);
final int gePrice = itemManager.getItemPrice(id);
final int alchPrice = itemComposition.getHaPrice();
if (gePrice > 0 || alchPrice > 0)
{
final ChatMessageBuilder message = new ChatMessageBuilder()
.append(ChatColorType.NORMAL)
.append("Price of ")
.append(ChatColorType.HIGHLIGHT);
if (quantity > 1)
{
message
.append(QuantityFormatter.formatNumber(quantity))
.append(" x ");
}
message
.append(itemComposition.getName())
.append(ChatColorType.NORMAL)
.append(":");
if (gePrice > 0)
{
message
.append(ChatColorType.NORMAL)
.append(" GE average ")
.append(ChatColorType.HIGHLIGHT)
.append(QuantityFormatter.formatNumber((long) gePrice * quantity));
if (quantity > 1)
{
message
.append(ChatColorType.NORMAL)
.append(" (")
.append(ChatColorType.HIGHLIGHT)
.append(QuantityFormatter.formatNumber(gePrice))
.append(ChatColorType.NORMAL)
.append("ea)");
}
}
if (alchPrice > 0)
{
message
.append(ChatColorType.NORMAL)
.append(" HA value ")
.append(ChatColorType.HIGHLIGHT)
.append(QuantityFormatter.formatNumber((long) alchPrice * quantity));
if (quantity > 1)
{
message
.append(ChatColorType.NORMAL)
.append(" (")
.append(ChatColorType.HIGHLIGHT)
.append(QuantityFormatter.formatNumber(alchPrice))
.append(ChatColorType.NORMAL)
.append("ea)");
}
}
chatMessageManager.queue(QueuedMessage.builder()
.type(ChatMessageType.ITEM_EXAMINE)
.runeLiteFormattedMessage(message.build())
.build());
}
}
private void submitExamine(PendingExamine examine, String text)
{
int id = examine.getId();
switch (examine.getType())
{
case ITEM:
examineClient.submitItem(id, text);
break;
case OBJECT:
examineClient.submitObject(id, text);
break;
case NPC:
examineClient.submitNpc(id, text);
break;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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.examine;
public enum ExamineType
{
ITEM,
ITEM_BANK_EQ,
NPC,
OBJECT;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* 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.examine;
import java.time.Instant;
import lombok.Data;
@Data
class PendingExamine
{
private ExamineType type;
private int id;
private int quantity;
private Instant created;
}

View File

@@ -0,0 +1,43 @@
/*
* 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.fairyring;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("fairyrings")
public interface FairyRingConfig extends Config
{
@ConfigItem(
keyName = "autoOpen",
name = "Open search automatically",
description = "Open the search widget every time you enter a fairy ring"
)
default boolean autoOpen()
{
return true;
}
}

View File

@@ -0,0 +1,367 @@
/*
* Copyright (c) 2018 Abex
* Copyright (c) 2017, Tyler <https://github.com/tylerthardy>
* Copyright (c) 2018, Yoav Ram <https://github.com/yoyo421>
* Copyright (c) 2018, Infinitay <https://github.com/Infinitay>
* 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.fairyring;
import com.google.common.base.Strings;
import com.google.inject.Provides;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
import javax.inject.Inject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.ScriptEvent;
import net.runelite.api.ScriptID;
import net.runelite.api.SoundEffectID;
import net.runelite.api.SpriteID;
import net.runelite.api.Varbits;
import net.runelite.api.widgets.WidgetType;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.VarbitChanged;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.JavaScriptCallback;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.game.chatbox.ChatboxPanelManager;
import net.runelite.client.game.chatbox.ChatboxTextInput;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.util.Text;
@Slf4j
@PluginDescriptor(
name = "Fairy Rings",
description = "Show the location of the fairy ring teleport",
tags = {"teleportation"}
)
public class FairyRingPlugin extends Plugin
{
private static final String[] leftDial = {"A", "D", "C", "B"};
private static final String[] middleDial = {"I", "L", "K", "J"};
private static final String[] rightDial = {"P", "S", "R", "Q"};
private static final int ENTRY_PADDING = 3;
private static final String MENU_OPEN = "Open";
private static final String MENU_CLOSE = "Close";
@Inject
private Client client;
@Inject
private FairyRingConfig config;
@Inject
private ChatboxPanelManager chatboxPanelManager;
@Inject
private ClientThread clientThread;
private ChatboxTextInput searchInput = null;
private Widget searchBtn;
private Collection<CodeWidgets> codes = null;
@Data
private static class CodeWidgets
{
// The fairy hideout has both of these null, because its not the same as the rest of them
@Nullable
private Widget favorite;
@Nullable
private Widget code;
private Widget description;
}
@Provides
FairyRingConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(FairyRingConfig.class);
}
@Subscribe
public void onVarbitChanged(VarbitChanged event)
{
setWidgetTextToDestination();
}
@Subscribe
public void onWidgetLoaded(WidgetLoaded widgetLoaded)
{
if (widgetLoaded.getGroupId() == WidgetID.FAIRY_RING_PANEL_GROUP_ID)
{
setWidgetTextToDestination();
Widget header = client.getWidget(WidgetInfo.FAIRY_RING_HEADER);
if (header != null)
{
searchBtn = header.createChild(-1, WidgetType.GRAPHIC);
searchBtn.setSpriteId(SpriteID.GE_SEARCH);
searchBtn.setOriginalWidth(17);
searchBtn.setOriginalHeight(17);
searchBtn.setOriginalX(11);
searchBtn.setOriginalY(11);
searchBtn.setHasListener(true);
searchBtn.setAction(1, MENU_OPEN);
searchBtn.setOnOpListener((JavaScriptCallback) this::menuOpen);
searchBtn.setName("Search");
searchBtn.revalidate();
codes = null;
if (config.autoOpen())
{
openSearch();
}
}
}
}
private void menuOpen(ScriptEvent e)
{
openSearch();
client.playSoundEffect(SoundEffectID.UI_BOOP);
}
private void menuClose(ScriptEvent e)
{
updateFilter("");
chatboxPanelManager.close();
client.playSoundEffect(SoundEffectID.UI_BOOP);
}
private void setWidgetTextToDestination()
{
Widget fairyRingTeleportButton = client.getWidget(WidgetInfo.FAIRY_RING_TELEPORT_BUTTON);
if (fairyRingTeleportButton != null && !fairyRingTeleportButton.isHidden())
{
String destination;
try
{
FairyRings fairyRingDestination = getFairyRingDestination(client.getVar(Varbits.FAIRY_RING_DIAL_ADCB),
client.getVar(Varbits.FAIRY_RIGH_DIAL_ILJK), client.getVar(Varbits.FAIRY_RING_DIAL_PSRQ));
destination = fairyRingDestination.getDestination();
}
catch (IllegalArgumentException ex)
{
destination = "Invalid location";
}
fairyRingTeleportButton.setText(destination);
}
}
private FairyRings getFairyRingDestination(int varbitValueDialLeft, int varbitValueDialMiddle, int varbitValueDialRight)
{
return FairyRings.valueOf(leftDial[varbitValueDialLeft] + middleDial[varbitValueDialMiddle] + rightDial[varbitValueDialRight]);
}
private void openSearch()
{
updateFilter("");
searchBtn.setAction(1, MENU_CLOSE);
searchBtn.setOnOpListener((JavaScriptCallback) this::menuClose);
searchInput = chatboxPanelManager.openTextInput("Filter fairy rings")
.onChanged(s -> clientThread.invokeLater(() -> updateFilter(s)))
.onDone(s -> false)
.onClose(() ->
{
clientThread.invokeLater(() -> updateFilter(""));
searchBtn.setOnOpListener((JavaScriptCallback) this::menuOpen);
searchBtn.setAction(1, MENU_OPEN);
})
.build();
}
@Subscribe
public void onGameTick(GameTick t)
{
// This has to happen because the only widget that gets hidden is the tli one
Widget fairyRingTeleportButton = client.getWidget(WidgetInfo.FAIRY_RING_TELEPORT_BUTTON);
boolean fairyRingWidgetOpen = fairyRingTeleportButton != null && !fairyRingTeleportButton.isHidden();
boolean chatboxOpen = searchInput != null && chatboxPanelManager.getCurrentInput() == searchInput;
if (!fairyRingWidgetOpen && chatboxOpen)
{
chatboxPanelManager.close();
}
}
private void updateFilter(String filter)
{
filter = filter.toLowerCase();
final Widget list = client.getWidget(WidgetInfo.FAIRY_RING_LIST);
final Widget favorites = client.getWidget(WidgetInfo.FAIRY_RING_FAVORITES);
if (list == null)
{
return;
}
if (codes != null)
{
// Check to make sure the list hasn't been rebuild since we were last her
// Do this by making sure the list's dynamic children are the same as when we last saw them
if (codes.stream().noneMatch(w ->
{
Widget codeWidget = w.getCode();
if (codeWidget == null)
{
return false;
}
return list.getChild(codeWidget.getIndex()) == codeWidget;
}))
{
codes = null;
}
}
if (codes == null)
{
// Find all of the widgets that we care about, grouping by their Y value
Map<Integer, CodeWidgets> codeMap = new TreeMap<>();
for (Widget w : list.getStaticChildren())
{
if (w.isSelfHidden())
{
continue;
}
if (w.getSpriteId() != -1)
{
codeMap.computeIfAbsent(w.getRelativeY(), k -> new CodeWidgets()).setFavorite(w);
}
else if (!Strings.isNullOrEmpty(w.getText()))
{
codeMap.computeIfAbsent(w.getRelativeY(), k -> new CodeWidgets()).setDescription(w);
}
}
for (Widget w : list.getDynamicChildren())
{
if (w.isSelfHidden())
{
continue;
}
CodeWidgets c = codeMap.computeIfAbsent(w.getRelativeY(), k -> new CodeWidgets());
c.setCode(w);
}
codes = codeMap.values();
}
// Relayout the panel
int y = 0;
if (favorites != null)
{
boolean hide = !filter.isEmpty();
favorites.setHidden(hide);
if (!hide)
{
y += favorites.getOriginalHeight() + ENTRY_PADDING;
}
}
for (CodeWidgets c : codes)
{
String code = Text.removeTags(c.getDescription().getName()).replaceAll(" ", "");
String tags = null;
if (!code.isEmpty())
{
try
{
FairyRings ring = FairyRings.valueOf(code);
tags = ring.getTags();
}
catch (IllegalArgumentException e)
{
log.warn("Unable to find ring with code '{}'", code, e);
}
}
boolean hidden = !(filter.isEmpty()
|| Text.removeTags(c.getDescription().getText()).toLowerCase().contains(filter)
|| code.toLowerCase().contains(filter)
|| tags != null && tags.contains(filter));
if (c.getCode() != null)
{
c.getCode().setHidden(hidden);
c.getCode().setOriginalY(y);
}
if (c.getFavorite() != null)
{
c.getFavorite().setHidden(hidden);
c.getFavorite().setOriginalY(y);
}
c.getDescription().setHidden(hidden);
c.getDescription().setOriginalY(y);
if (!hidden)
{
y += c.getDescription().getHeight() + ENTRY_PADDING;
}
}
y -= ENTRY_PADDING;
if (y < 0)
{
y = 0;
}
int newHeight = 0;
if (list.getScrollHeight() > 0)
{
newHeight = (list.getScrollY() * y) / list.getScrollHeight();
}
list.setScrollHeight(y);
list.revalidateScroll();
client.runScript(
ScriptID.UPDATE_SCROLLBAR,
WidgetInfo.FAIRY_RING_LIST_SCROLLBAR.getId(),
WidgetInfo.FAIRY_RING_LIST.getId(),
newHeight
);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (c) 2018, Yoav Ram <https://github.com/yoyo421>
* Copyright (c) 2018, Infinitay <https://github.com/Infinitay>
* 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.fairyring;
import lombok.Getter;
public enum FairyRings
{
// A
AIQ("Mudskipper Point"),
AIR("(Island) South-east of Ardougne"),
AJQ("Cave south of Dorgesh-Kaan"),
AJR("Slayer cave"),
AJS("Penguins near Miscellania"),
AKQ("Piscatoris Hunter area"),
AKS("Feldip Hunter area"),
ALP("(Island) Lighthouse"),
ALQ("Haunted Woods east of Canifis"),
ALR("Abyssal Area"),
ALS("McGrubor's Wood"),
// B
BIP("(Island) South-west of Mort Myre"),
BIQ("Kalphite Hive"),
BIS("Ardougne Zoo - Unicorns"),
BJR("Realm of the Fisher King"),
BJS("(Island) Near Zul-Andra", "zulrah"),
BKP("South of Castle Wars"),
BKQ("Enchanted Valley"),
BKR("Mort Myre Swamp, south of Canifis"),
BKS("Zanaris"),
BLP("TzHaar area"),
BLR("Legends' Guild"),
// C
CIP("(Island) Miscellania"),
CIQ("North-west of Yanille"),
CIS("North of the Arceuus Library"),
CIR("North-east of the Farming Guild", "mount karuulm konar"),
CJR("Sinclair Mansion", "falo bard"),
CKP("Cosmic entity's plane"),
CKR("South of Tai Bwo Wannai Village"),
CKS("Canifis"),
CLP("(Island) South of Draynor Village"),
CLR("(Island) Ape Atoll"),
CLS("(Island) Hazelmere's home"),
// D
DIP("(Sire Boss) Abyssal Nexus"),
DIR("Gorak's Plane"),
DIQ("Player-owned house", "poh home"),
DIS("Wizards' Tower"),
DJP("Tower of Life"),
DJR("Chasm of Fire"),
DKP("South of Musa Point"),
DKR("Edgeville, Grand Exchange"),
DKS("Polar Hunter area"),
DLQ("North of Nardah"),
DLR("(Island) Poison Waste south of Isafdar"),
DLS("Myreque hideout under The Hollows");
@Getter
private final String destination;
@Getter
private final String tags;
FairyRings(String destination)
{
this(destination, "");
}
FairyRings(String destination, String tags)
{
this.destination = destination;
this.tags = tags.toLowerCase() + " " + destination.toLowerCase();
}
}

View File

@@ -0,0 +1,42 @@
package net.runelite.client.plugins.feed;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("feed")
public interface FeedConfig extends Config
{
@ConfigItem(
keyName = "includeBlogPosts",
name = "Include Blog Posts",
description = "Configures whether blog posts are displayed",
position = 0
)
default boolean includeBlogPosts()
{
return true;
}
@ConfigItem(
keyName = "includeTweets",
name = "Include Tweets",
description = "Configures whether tweets are displayed",
position = 1
)
default boolean includeTweets()
{
return true;
}
@ConfigItem(
keyName = "includeOsrsNews",
name = "Include OSRS News",
description = "Configures whether OSRS news are displayed",
position = 2
)
default boolean includeOsrsNews()
{
return true;
}
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* Copyright (c) 2018, Psikoi <https://github.com/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.feed;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Comparator;
import java.util.function.Supplier;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.LinkBrowser;
import net.runelite.http.api.feed.FeedItem;
import net.runelite.http.api.feed.FeedItemType;
import net.runelite.http.api.feed.FeedResult;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
@Slf4j
@Singleton
class FeedPanel extends PluginPanel
{
private static final ImageIcon RUNELITE_ICON;
private static final ImageIcon OSRS_ICON;
private static final Color TWEET_BACKGROUND = new Color(15, 15, 15);
private static final Color OSRS_NEWS_BACKGROUND = new Color(36, 30, 19);
private static final Color BLOG_POST_BACKGROUND = new Color(11, 30, 41);
private static final int MAX_CONTENT_LINES = 3;
private static final int CONTENT_WIDTH = 148;
private static final int TIME_WIDTH = 20;
private static final Comparator<FeedItem> FEED_ITEM_COMPARATOR = (o1, o2) ->
{
if (o1.getType() != o2.getType())
{
if (o1.getType() == FeedItemType.BLOG_POST)
{
return -1;
}
else if (o2.getType() == FeedItemType.BLOG_POST)
{
return 1;
}
}
return -Long.compare(o1.getTimestamp(), o2.getTimestamp());
};
static
{
RUNELITE_ICON = new ImageIcon(ImageUtil.getResourceStreamFromClass(FeedPanel.class, "runelite.png"));
OSRS_ICON = new ImageIcon(ImageUtil.getResourceStreamFromClass(FeedPanel.class, "osrs.png"));
}
private final FeedConfig config;
private final Supplier<FeedResult> feedSupplier;
private final OkHttpClient okHttpClient;
@Inject
FeedPanel(FeedConfig config, Supplier<FeedResult> feedSupplier, OkHttpClient okHttpClient)
{
super(true);
this.config = config;
this.feedSupplier = feedSupplier;
this.okHttpClient = okHttpClient;
setBorder(new EmptyBorder(10, 10, 10, 10));
setBackground(ColorScheme.DARK_GRAY_COLOR);
setLayout(new GridLayout(0, 1, 0, 4));
}
void rebuildFeed()
{
FeedResult feed = feedSupplier.get();
if (feed == null)
{
return;
}
SwingUtilities.invokeLater(() ->
{
removeAll();
feed.getItems()
.stream()
.filter(f -> f.getType() != FeedItemType.BLOG_POST || config.includeBlogPosts())
.filter(f -> f.getType() != FeedItemType.TWEET || config.includeTweets())
.filter(f -> f.getType() != FeedItemType.OSRS_NEWS || config.includeOsrsNews())
.sorted(FEED_ITEM_COMPARATOR)
.forEach(this::addItemToPanel);
});
}
private void addItemToPanel(FeedItem item)
{
JPanel avatarAndRight = new JPanel(new BorderLayout());
avatarAndRight.setPreferredSize(new Dimension(0, 56));
JLabel avatar = new JLabel();
// width = 48+4 to compensate for the border
avatar.setPreferredSize(new Dimension(52, 48));
avatar.setBorder(new EmptyBorder(0, 4, 0, 0));
switch (item.getType())
{
case TWEET:
try
{
Request request = new Request.Builder()
.url(item.getAvatar())
.build();
okHttpClient.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e)
{
log.warn(null, e);
}
@Override
public void onResponse(Call call, Response response) throws IOException
{
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
{
log.warn("Failed to download image " + item.getAvatar());
return;
}
BufferedImage icon;
synchronized (ImageIO.class)
{
icon = ImageIO.read(responseBody.byteStream());
}
avatar.setIcon(new ImageIcon(icon));
}
}
});
}
catch (IllegalArgumentException | NullPointerException e)
{
log.warn(null, e);
}
avatarAndRight.setBackground(TWEET_BACKGROUND);
break;
case OSRS_NEWS:
if (OSRS_ICON != null)
{
avatar.setIcon(OSRS_ICON);
}
avatarAndRight.setBackground(OSRS_NEWS_BACKGROUND);
break;
default:
if (RUNELITE_ICON != null)
{
avatar.setIcon(RUNELITE_ICON);
}
avatarAndRight.setBackground(BLOG_POST_BACKGROUND);
break;
}
JPanel upAndContent = new JPanel();
upAndContent.setLayout(new BoxLayout(upAndContent, BoxLayout.Y_AXIS));
upAndContent.setBorder(new EmptyBorder(4, 8, 4, 4));
upAndContent.setBackground(null);
JPanel titleAndTime = new JPanel();
titleAndTime.setLayout(new BorderLayout());
titleAndTime.setBackground(null);
Color darkerForeground = UIManager.getColor("Label.foreground").darker();
JLabel titleLabel = new JLabel(item.getTitle());
titleLabel.setFont(FontManager.getRunescapeSmallFont());
titleLabel.setBackground(null);
titleLabel.setForeground(darkerForeground);
titleLabel.setPreferredSize(new Dimension(CONTENT_WIDTH - TIME_WIDTH, 0));
Duration duration = Duration.between(Instant.ofEpochMilli(item.getTimestamp()), Instant.now());
JLabel timeLabel = new JLabel(durationToString(duration));
timeLabel.setFont(FontManager.getRunescapeSmallFont());
timeLabel.setForeground(darkerForeground);
titleAndTime.add(titleLabel, BorderLayout.WEST);
titleAndTime.add(timeLabel, BorderLayout.EAST);
JPanel content = new JPanel(new BorderLayout());
content.setBackground(null);
JLabel contentLabel = new JLabel(lineBreakText(item.getContent(), FontManager.getRunescapeSmallFont()));
contentLabel.setBorder(new EmptyBorder(2, 0, 0, 0));
contentLabel.setFont(FontManager.getRunescapeSmallFont());
contentLabel.setForeground(darkerForeground);
content.add(contentLabel, BorderLayout.CENTER);
upAndContent.add(titleAndTime);
upAndContent.add(content);
upAndContent.add(new Box.Filler(new Dimension(0, 0),
new Dimension(0, Short.MAX_VALUE),
new Dimension(0, Short.MAX_VALUE)));
avatarAndRight.add(avatar, BorderLayout.WEST);
avatarAndRight.add(upAndContent, BorderLayout.CENTER);
Color backgroundColor = avatarAndRight.getBackground();
Color hoverColor = backgroundColor.brighter().brighter();
Color pressedColor = hoverColor.brighter();
avatarAndRight.addMouseListener(new MouseAdapter()
{
@Override
public void mouseEntered(MouseEvent e)
{
avatarAndRight.setBackground(hoverColor);
avatarAndRight.setCursor(new Cursor(Cursor.HAND_CURSOR));
}
@Override
public void mouseExited(MouseEvent e)
{
avatarAndRight.setBackground(backgroundColor);
avatarAndRight.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
@Override
public void mousePressed(MouseEvent e)
{
avatarAndRight.setBackground(pressedColor);
}
@Override
public void mouseReleased(MouseEvent e)
{
avatarAndRight.setBackground(hoverColor);
LinkBrowser.browse(item.getUrl());
}
});
add(avatarAndRight);
}
private String durationToString(Duration duration)
{
if (duration.getSeconds() >= 60 * 60 * 24)
{
return (int) (duration.getSeconds() / (60 * 60 * 24)) + "d";
}
else if (duration.getSeconds() >= 60 * 60)
{
return (int) (duration.getSeconds() / (60 * 60)) + "h";
}
return (int) (duration.getSeconds() / 60) + "m";
}
private String lineBreakText(String text, Font font)
{
StringBuilder newText = new StringBuilder("<html>");
FontRenderContext fontRenderContext = new FontRenderContext(font.getTransform(),
true, true);
int lines = 0;
int pos = 0;
String[] words = text.split(" ");
String line = "";
while (lines < MAX_CONTENT_LINES && pos < words.length)
{
String newLine = pos > 0 ? line + " " + words[pos] : words[pos];
double width = font.getStringBounds(newLine, fontRenderContext).getWidth();
if (width >= CONTENT_WIDTH)
{
newText.append(line);
newText.append("<br>");
line = "";
lines++;
}
else
{
line = newLine;
pos++;
}
}
newText.append(line);
newText.append("</html>");
return newText.toString();
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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.feed;
import com.google.common.base.Suppliers;
import com.google.inject.Binder;
import com.google.inject.Provides;
import com.google.inject.TypeLiteral;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.task.Schedule;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.util.ImageUtil;
import net.runelite.http.api.feed.FeedClient;
import net.runelite.http.api.feed.FeedResult;
import okhttp3.OkHttpClient;
@PluginDescriptor(
name = "News Feed",
description = "Show the latest RuneLite blog posts, OSRS news, and JMod Twitter posts",
tags = {"external", "integration", "panel", "twitter"},
loadWhenOutdated = true
)
@Slf4j
public class FeedPlugin extends Plugin
{
@Inject
private ClientToolbar clientToolbar;
@Inject
private ScheduledExecutorService executorService;
@Inject
private FeedClient feedClient;
private FeedPanel feedPanel;
private NavigationButton navButton;
private final Supplier<FeedResult> feedSupplier = Suppliers.memoizeWithExpiration(() ->
{
try
{
return feedClient.lookupFeed();
}
catch (IOException e)
{
log.warn(null, e);
}
return null;
}, 10, TimeUnit.MINUTES);
@Override
public void configure(Binder binder)
{
// CHECKSTYLE:OFF
binder.bind(new TypeLiteral<Supplier<FeedResult>>(){})
.toInstance(feedSupplier);
// CHECKSTYLE:ON
}
@Override
protected void startUp() throws Exception
{
feedPanel = injector.getInstance(FeedPanel.class);
final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "icon.png");
navButton = NavigationButton.builder()
.tooltip("News Feed")
.icon(icon)
.priority(8)
.panel(feedPanel)
.build();
clientToolbar.addNavigation(navButton);
executorService.submit(this::updateFeed);
}
@Override
protected void shutDown() throws Exception
{
clientToolbar.removeNavigation(navButton);
}
private void updateFeed()
{
feedPanel.rebuildFeed();
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("feed"))
{
executorService.submit(this::updateFeed);
}
}
@Schedule(
period = 10,
unit = ChronoUnit.MINUTES,
asynchronous = true
)
public void updateFeedTask()
{
updateFeed();
}
@Provides
FeedConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(FeedConfig.class);
}
@Provides
FeedClient provideFeedClient(OkHttpClient okHttpClient)
{
return new FeedClient(okHttpClient);
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2017, Levi <me@levischuck.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.fps;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Range;
@ConfigGroup(FpsPlugin.CONFIG_GROUP_KEY)
public interface FpsConfig extends Config
{
@ConfigItem(
keyName = "limitFps",
name = "Limit Global FPS",
description = "Global FPS limit in effect regardless of<br>" +
"whether window is in focus or not",
position = 1
)
default boolean limitFps()
{
return false;
}
@Range(
min = 1,
max = 50
)
@ConfigItem(
keyName = "maxFps",
name = "Global FPS target",
description = "Desired max global frames per second",
position = 2
)
default int maxFps()
{
return 50;
}
@ConfigItem(
keyName = "limitFpsUnfocused",
name = "Limit FPS unfocused",
description = "FPS limit while window is out of focus",
position = 3
)
default boolean limitFpsUnfocused()
{
return false;
}
@Range(
min = 1,
max = 50
)
@ConfigItem(
keyName = "maxFpsUnfocused",
name = "Unfocused FPS target",
description = "Desired max frames per second for unfocused",
position = 4
)
default int maxFpsUnfocused()
{
return 50;
}
@ConfigItem(
keyName = "drawFps",
name = "Draw FPS indicator",
description = "Show a number in the corner for the current FPS",
position = 5
)
default boolean drawFps()
{
return true;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright (c) 2017, Levi <me@levischuck.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.fps;
import javax.inject.Inject;
import net.runelite.api.events.FocusChanged;
/**
* FPS Draw Listener runs after the canvas has been painted with the buffered image (unused in this plugin)
* It specifically is designed only to pause when RS decides to paint the canvas, after the canvas has been painted.
* The RS client operates at 50 cycles happen per second in the RS Client with higher priority than draws per second.
* For high powered computers, drawing is limited by client cycles, so 50 is the max FPS observed.
* After running a game cycle and a draw operation, the RS client burns the rest of the time with a nano timer.
* For low powered computers, the RS client is often throttled by the hardware or OS and draws at 25-30 fps.
* The nano timer is not used in this scenario.
* Instead to catch up the RS client runs several cycles before drawing, thus maintaining 50 cycles / second.
* <p>
* Enforcing FPS in the draw code does not impact the client engine's ability to run including its audio,
* even when forced to 1 FPS with this plugin.
*/
public class FpsDrawListener implements Runnable
{
private static final int SAMPLE_SIZE = 4;
private final FpsConfig config;
private long targetDelay = 0;
// Often changing values
private boolean isFocused = true;
// Working set
private long lastMillis = 0;
private long[] lastDelays = new long[SAMPLE_SIZE];
private int lastDelayIndex = 0;
private long sleepDelay = 0;
@Inject
private FpsDrawListener(FpsConfig config)
{
this.config = config;
reloadConfig();
}
void reloadConfig()
{
lastMillis = System.currentTimeMillis();
int fps = config.limitFpsUnfocused() && !isFocused
? config.maxFpsUnfocused()
: config.maxFps();
targetDelay = 1000 / Math.max(1, fps);
sleepDelay = targetDelay;
for (int i = 0; i < SAMPLE_SIZE; i++)
{
lastDelays[i] = targetDelay;
}
}
void onFocusChanged(FocusChanged event)
{
this.isFocused = event.isFocused();
reloadConfig(); // load new delay
}
private boolean isEnforced()
{
return config.limitFps()
|| (config.limitFpsUnfocused() && !isFocused);
}
@Override
public void run()
{
if (!isEnforced())
{
return;
}
// We can't trust client.getFPS to get frame-perfect FPS knowledge
// If we do try to use client.getFPS, we will end up oscillating
// So we rely on currentTimeMillis which is occasionally cached by the JVM unlike nanotime
// Its caching will not cause oscillation as it is granular enough for our uses here
final long before = lastMillis;
final long now = System.currentTimeMillis();
lastMillis = now;
lastDelayIndex = (lastDelayIndex + 1) % SAMPLE_SIZE;
lastDelays[lastDelayIndex] = now - before;
// We take a sampling approach because sometimes the game client seems to repaint
// after only running 1 game cycle, and then runs repaint again after running 30 cycles
long averageDelay = 0;
for (int i = 0; i < SAMPLE_SIZE; i++)
{
averageDelay += lastDelays[i];
}
averageDelay /= lastDelays.length;
// Sleep delay is a moving target due to the nature of how and when the engine
// decides to run cycles.
// This will also keep us safe from time spent in plugins conditionally
// as some plugins and overlays are only appropriate in some game areas
if (averageDelay > targetDelay)
{
sleepDelay--;
}
else if (averageDelay < targetDelay)
{
sleepDelay++;
}
if (sleepDelay > 0)
{
try
{
Thread.sleep(sleepDelay);
}
catch (InterruptedException e)
{
// Can happen on shutdown
}
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (c) 2017, Levi <me@levischuck.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.fps;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.Point;
import net.runelite.api.events.FocusChanged;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayPriority;
import net.runelite.client.ui.overlay.OverlayUtil;
/**
* The built in FPS overlay has a few problems that this one does not have, most of all: it is distracting.
* 1. The built in one also shows memory, which constantly fluctuates and garbage collects.
* It is useful for devs profiling memory.
* 2. The built in one shifts around constantly because it is not monospace.
* This locks "FPS:" into one position (the far top right corner of the canvas),
* along with a locked position for the FPS value.
*/
public class FpsOverlay extends Overlay
{
private static final int Y_OFFSET = 1;
private static final int X_OFFSET = 1;
private static final String FPS_STRING = " FPS";
// Local dependencies
private final FpsConfig config;
private final Client client;
// Often changing values
private boolean isFocused = true;
@Inject
private FpsOverlay(FpsConfig config, Client client)
{
this.config = config;
this.client = client;
setLayer(OverlayLayer.ABOVE_WIDGETS);
setPriority(OverlayPriority.HIGH);
setPosition(OverlayPosition.DYNAMIC);
}
void onFocusChanged(FocusChanged event)
{
isFocused = event.isFocused();
}
private boolean isEnforced()
{
return config.limitFps()
|| (config.limitFpsUnfocused() && !isFocused);
}
private Color getFpsValueColor()
{
return isEnforced() ? Color.red : Color.yellow;
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!config.drawFps())
{
return null;
}
// On resizable bottom line mode the logout button is at the top right, so offset the overlay
// to account for it
Widget logoutButton = client.getWidget(WidgetInfo.RESIZABLE_MINIMAP_LOGOUT_BUTTON);
int xOffset = X_OFFSET;
if (logoutButton != null && !logoutButton.isHidden())
{
xOffset += logoutButton.getWidth();
}
final String text = client.getFPS() + FPS_STRING;
final int textWidth = graphics.getFontMetrics().stringWidth(text);
final int textHeight = graphics.getFontMetrics().getAscent() - graphics.getFontMetrics().getDescent();
final int width = (int) client.getRealDimensions().getWidth();
final Point point = new Point(width - textWidth - xOffset, textHeight + Y_OFFSET);
OverlayUtil.renderTextLocation(graphics, point, text, getFpsValueColor());
return null;
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (c) 2017, Levi <me@levischuck.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.fps;
import com.google.inject.Inject;
import com.google.inject.Provides;
import net.runelite.client.events.ConfigChanged;
import net.runelite.api.events.FocusChanged;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.DrawManager;
import net.runelite.client.ui.overlay.OverlayManager;
/**
* FPS Control has two primary areas, this plugin class just keeps those areas up to date and handles setup / teardown.
*
* <p>Overlay paints the current FPS, the color depends on whether or not FPS is being enforced.
* The overlay is lightweight and is merely and indicator.
*
* <p>Draw Listener, sleeps a calculated amount after each canvas paint operation.
* This is the heart of the plugin, the amount of sleep taken is regularly adjusted to account varying
* game and system load, it usually finds the sweet spot in about two seconds.
*/
@PluginDescriptor(
name = "FPS Control",
description = "Show current FPS and/or set an FPS limit",
tags = {"frames", "framerate", "limit", "overlay"},
enabledByDefault = false
)
public class FpsPlugin extends Plugin
{
static final String CONFIG_GROUP_KEY = "fpscontrol";
@Inject
private OverlayManager overlayManager;
@Inject
private FpsOverlay overlay;
@Inject
private FpsDrawListener drawListener;
@Inject
private DrawManager drawManager;
@Provides
FpsConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(FpsConfig.class);
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals(CONFIG_GROUP_KEY))
{
drawListener.reloadConfig();
}
}
@Subscribe
public void onFocusChanged(FocusChanged event)
{
drawListener.onFocusChanged(event);
overlay.onFocusChanged(event);
}
@Override
protected void startUp() throws Exception
{
overlayManager.add(overlay);
drawManager.registerEveryFrameListener(drawListener);
drawListener.reloadConfig();
}
@Override
protected void shutDown() throws Exception
{
overlayManager.remove(overlay);
drawManager.unregisterEveryFrameListener(drawListener);
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (c) 2018, Connor <contact@connor-parks.email>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.friendlist;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.Friend;
import net.runelite.api.Ignore;
import net.runelite.api.NameableContainer;
import net.runelite.api.ScriptID;
import net.runelite.api.VarPlayer;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
@PluginDescriptor(
name = "Friend List",
description = "Add extra information to the friend and ignore lists"
)
public class FriendListPlugin extends Plugin
{
private static final int MAX_FRIENDS_P2P = 400;
private static final int MAX_FRIENDS_F2P = 200;
private static final int MAX_IGNORES_P2P = 400;
private static final int MAX_IGNORES_F2P = 100;
@Inject
private Client client;
@Override
protected void shutDown()
{
final int world = client.getWorld();
setFriendsListTitle("Friends List - World " + world);
setIgnoreListTitle("Ignore List - World " + world);
}
@Subscribe
public void onScriptPostFired(ScriptPostFired event)
{
if (event.getScriptId() == ScriptID.FRIENDS_UPDATE)
{
final int world = client.getWorld();
final boolean isMember = client.getVar(VarPlayer.MEMBERSHIP_DAYS) > 0;
final NameableContainer<Friend> friendContainer = client.getFriendContainer();
final int friendCount = friendContainer.getCount();
if (friendCount >= 0)
{
final int limit = isMember ? MAX_FRIENDS_P2P : MAX_FRIENDS_F2P;
final String title = "Friends - W" +
world +
" (" +
friendCount +
"/" +
limit +
")";
setFriendsListTitle(title);
}
}
else if (event.getScriptId() == ScriptID.IGNORE_UPDATE)
{
final int world = client.getWorld();
final boolean isMember = client.getVar(VarPlayer.MEMBERSHIP_DAYS) > 0;
final NameableContainer<Ignore> ignoreContainer = client.getIgnoreContainer();
final int ignoreCount = ignoreContainer.getCount();
if (ignoreCount >= 0)
{
final int limit = isMember ? MAX_IGNORES_P2P : MAX_IGNORES_F2P;
final String title = "Ignores - W" +
world +
" (" +
ignoreCount +
"/" +
limit +
")";
setIgnoreListTitle(title);
}
}
}
private void setFriendsListTitle(final String title)
{
Widget friendListTitleWidget = client.getWidget(WidgetInfo.FRIEND_CHAT_TITLE);
if (friendListTitleWidget != null)
{
friendListTitleWidget.setText(title);
}
}
private void setIgnoreListTitle(final String title)
{
Widget ignoreTitleWidget = client.getWidget(WidgetInfo.IGNORE_TITLE);
if (ignoreTitleWidget != null)
{
ignoreTitleWidget.setText(title);
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2018, trimbe <github.com/trimbe>
* 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.friendschat;
enum ActivityType
{
JOINED,
LEFT
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright (c) 2017, Ron <https://github.com/raiyni>
* 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.friendschat;
import java.awt.Color;
import net.runelite.api.FriendsChatRank;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("clanchat") // group name from the old plugin
public interface FriendsChatConfig extends Config
{
@ConfigItem(
keyName = "clanChatIcons",
name = "Chat Icons",
description = "Show rank icons next to friends chat members.",
position = 1
)
default boolean chatIcons()
{
return true;
}
@ConfigItem(
keyName = "recentChats",
name = "Recent Chats",
description = "Show recent friends chats.",
position = 2
)
default boolean recentChats()
{
return true;
}
@ConfigItem(
keyName = "clanCounter",
name = "Members Counter",
description = "Show the amount of friends chat members near you.",
position = 3
)
default boolean showCounter()
{
return false;
}
@ConfigItem(
keyName = "chatsData",
name = "",
description = "",
hidden = true
)
default String chatsData()
{
return "";
}
@ConfigItem(
keyName = "chatsData",
name = "",
description = ""
)
void chatsData(String str);
@ConfigItem(
keyName = "showJoinLeave",
name = "Show Join/Leave",
description = "Adds a temporary message notifying when a member joins or leaves.",
position = 4
)
default boolean showJoinLeave()
{
return false;
}
@ConfigItem(
keyName = "joinLeaveRank",
name = "Join/Leave rank",
description = "Only show join/leave messages for members at or above this rank.",
position = 5
)
default FriendsChatRank joinLeaveRank()
{
return FriendsChatRank.UNRANKED;
}
@ConfigItem(
keyName = "joinLeaveTimeout",
name = "Join/Leave timeout",
description = "Set the timeout duration of join/leave messages. A value of 0 will make the messages permanent.",
position = 6
)
default int joinLeaveTimeout()
{
return 20;
}
@ConfigItem(
keyName = "privateMessageIcons",
name = "Private Message Icons",
description = "Add rank icons to private messages received from members.",
position = 7
)
default boolean privateMessageIcons()
{
return false;
}
@ConfigItem(
keyName = "publicChatIcons",
name = "Public Chat Icons",
description = "Add rank icons to public chat messages from members.",
position = 8
)
default boolean publicChatIcons()
{
return false;
}
@ConfigItem(
keyName = "clanTabChat",
name = "Tab Chat",
description = "Message friends chat without appending '/' when the friends chat tab is selected.",
position = 9
)
default boolean friendsChatTabChat()
{
return false;
}
@ConfigItem(
keyName = "confirmKicks",
name = "Confirm Kicks",
description = "Shows a chat prompt to confirm kicks",
position = 10
)
default boolean confirmKicks()
{
return false;
}
@ConfigItem(
keyName = "showIgnores",
name = "Recolor ignored players",
description = "Recolor members who are on your ignore list",
position = 11
)
default boolean showIgnores()
{
return true;
}
@ConfigItem(
keyName = "showIgnoresColor",
name = "Ignored color",
description = "Allows you to change the color of the ignored players in your friends chat",
position = 12
)
default Color showIgnoresColor()
{
return Color.RED;
}
}

View File

@@ -0,0 +1,722 @@
/*
* Copyright (c) 2017, Devin French <https://github.com/devinfrench>
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* Copyright (c) 2018, trimbe <github.com/trimbe>
* 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.friendschat;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Runnables;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import net.runelite.api.ChatLineBuffer;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.FriendsChatManager;
import net.runelite.api.FriendsChatMember;
import net.runelite.api.FriendsChatRank;
import net.runelite.api.GameState;
import net.runelite.api.Ignore;
import net.runelite.api.MessageNode;
import net.runelite.api.NameableContainer;
import net.runelite.api.Player;
import net.runelite.api.ScriptID;
import net.runelite.api.SpriteID;
import net.runelite.api.VarClientStr;
import net.runelite.api.Varbits;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.FriendsChatChanged;
import net.runelite.api.events.FriendsChatMemberJoined;
import net.runelite.api.events.FriendsChatMemberLeft;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.PlayerDespawned;
import net.runelite.api.events.PlayerSpawned;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.VarClientStrChanged;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.api.widgets.WidgetType;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.game.FriendChatManager;
import net.runelite.client.game.SpriteManager;
import net.runelite.client.game.chatbox.ChatboxPanelManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import static net.runelite.client.ui.JagexColors.CHAT_FC_NAME_OPAQUE_BACKGROUND;
import static net.runelite.client.ui.JagexColors.CHAT_FC_NAME_TRANSPARENT_BACKGROUND;
import static net.runelite.client.ui.JagexColors.CHAT_FC_TEXT_OPAQUE_BACKGROUND;
import static net.runelite.client.ui.JagexColors.CHAT_FC_TEXT_TRANSPARENT_BACKGROUND;
import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
import net.runelite.client.util.Text;
@PluginDescriptor(
name = "Friends Chat",
description = "Add rank icons to users talking in friends chat",
tags = {"icons", "rank", "recent", "clan"}
)
public class FriendsChatPlugin extends Plugin
{
private static final int MAX_CHATS = 10;
private static final String TITLE = "FC";
private static final String RECENT_TITLE = "Recent FCs";
private static final int MESSAGE_DELAY = 10;
@Inject
private Client client;
@Inject
private FriendChatManager friendChatManager;
@Inject
private FriendsChatConfig config;
@Inject
private InfoBoxManager infoBoxManager;
@Inject
private SpriteManager spriteManager;
@Inject
private ClientThread clientThread;
@Inject
private ChatboxPanelManager chatboxPanelManager;
private List<String> chats = new ArrayList<>();
private final List<Player> members = new ArrayList<>();
private MembersIndicator membersIndicator;
/**
* queue of temporary messages added to the client
*/
private final Deque<MemberJoinMessage> joinMessages = new ArrayDeque<>();
private final Map<String, MemberActivity> activityBuffer = new HashMap<>();
private int joinedTick;
private boolean kickConfirmed = false;
@Provides
FriendsChatConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(FriendsChatConfig.class);
}
@Override
public void startUp()
{
chats = new ArrayList<>(Text.fromCSV(config.chatsData()));
if (config.showIgnores())
{
clientThread.invoke(() -> colorIgnoredPlayers(config.showIgnoresColor()));
}
}
@Override
public void shutDown()
{
clientThread.invoke(() -> colorIgnoredPlayers(Color.WHITE));
members.clear();
resetCounter();
resetChats();
}
@Subscribe
public void onConfigChanged(ConfigChanged configChanged)
{
if (configChanged.getGroup().equals("clanchat"))
{
if (!config.recentChats())
{
resetChats();
}
if (config.showCounter())
{
clientThread.invoke(this::addCounter);
}
else
{
resetCounter();
}
Color ignoreColor = config.showIgnores() ? config.showIgnoresColor() : Color.WHITE;
clientThread.invoke(() -> colorIgnoredPlayers(ignoreColor));
}
}
@Subscribe
public void onFriendsChatMemberJoined(FriendsChatMemberJoined event)
{
final FriendsChatMember member = event.getMember();
if (member.getWorld() == client.getWorld())
{
final Player local = client.getLocalPlayer();
final String memberName = Text.toJagexName(member.getName());
for (final Player player : client.getPlayers())
{
if (player != null && player != local && memberName.equals(Text.toJagexName(player.getName())))
{
members.add(player);
addCounter();
break;
}
}
}
// members getting initialized isn't relevant
if (joinedTick == client.getTickCount())
{
return;
}
if (!config.showJoinLeave() ||
member.getRank().getValue() < config.joinLeaveRank().getValue())
{
return;
}
// attempt to filter out world hopping joins
if (!activityBuffer.containsKey(member.getName()))
{
MemberActivity joinActivity = new MemberActivity(ActivityType.JOINED,
member, client.getTickCount());
activityBuffer.put(member.getName(), joinActivity);
}
else
{
activityBuffer.remove(member.getName());
}
}
@Subscribe
public void onFriendsChatMemberLeft(FriendsChatMemberLeft event)
{
final FriendsChatMember member = event.getMember();
if (member.getWorld() == client.getWorld())
{
final String memberName = Text.toJagexName(member.getName());
final Iterator<Player> each = members.iterator();
while (each.hasNext())
{
if (memberName.equals(Text.toJagexName(each.next().getName())))
{
each.remove();
if (members.isEmpty())
{
resetCounter();
}
break;
}
}
}
if (!config.showJoinLeave() ||
member.getRank().getValue() < config.joinLeaveRank().getValue())
{
return;
}
if (!activityBuffer.containsKey(member.getName()))
{
MemberActivity leaveActivity = new MemberActivity(ActivityType.LEFT,
member, client.getTickCount());
activityBuffer.put(member.getName(), leaveActivity);
}
else
{
activityBuffer.remove(member.getName());
}
}
@Subscribe
public void onGameTick(GameTick gameTick)
{
if (client.getGameState() != GameState.LOGGED_IN)
{
return;
}
Widget chatTitleWidget = client.getWidget(WidgetInfo.FRIENDS_CHAT_TITLE);
if (chatTitleWidget != null)
{
Widget chatList = client.getWidget(WidgetInfo.FRIENDS_CHAT_LIST);
Widget owner = client.getWidget(WidgetInfo.FRIENDS_CHAT_OWNER);
FriendsChatManager friendsChatManager = client.getFriendsChatManager();
if (friendsChatManager != null && friendsChatManager.getCount() > 0)
{
chatTitleWidget.setText(TITLE + " (" + friendsChatManager.getCount() + "/100)");
}
else if (config.recentChats() && chatList.getChildren() == null && !Strings.isNullOrEmpty(owner.getText()))
{
chatTitleWidget.setText(RECENT_TITLE);
loadFriendsChats();
}
}
if (!config.showJoinLeave())
{
return;
}
timeoutMessages();
addActivityMessages();
}
private void timeoutMessages()
{
if (joinMessages.isEmpty())
{
return;
}
final int joinLeaveTimeout = config.joinLeaveTimeout();
if (joinLeaveTimeout == 0)
{
return;
}
boolean removed = false;
for (Iterator<MemberJoinMessage> it = joinMessages.iterator(); it.hasNext(); )
{
MemberJoinMessage joinMessage = it.next();
MessageNode messageNode = joinMessage.getMessageNode();
final int createdTick = joinMessage.getTick();
if (client.getTickCount() > createdTick + joinLeaveTimeout)
{
it.remove();
// If this message has been reused since, it will get a different id
if (joinMessage.getGetMessageId() == messageNode.getId())
{
ChatLineBuffer ccInfoBuffer = client.getChatLineMap().get(ChatMessageType.FRIENDSCHATNOTIFICATION.getType());
if (ccInfoBuffer != null)
{
ccInfoBuffer.removeMessageNode(messageNode);
removed = true;
}
}
}
else
{
// Everything else in the deque is newer
break;
}
}
if (removed)
{
clientThread.invoke(() -> client.runScript(ScriptID.BUILD_CHATBOX));
}
}
private void addActivityMessages()
{
FriendsChatManager friendsChatManager = client.getFriendsChatManager();
if (friendsChatManager == null || activityBuffer.isEmpty())
{
return;
}
Iterator<MemberActivity> activityIt = activityBuffer.values().iterator();
while (activityIt.hasNext())
{
MemberActivity activity = activityIt.next();
if (activity.getTick() < client.getTickCount() - MESSAGE_DELAY)
{
activityIt.remove();
addActivityMessage(friendsChatManager, activity.getMember(), activity.getActivityType());
}
}
}
private void addActivityMessage(FriendsChatManager friendsChatManager, FriendsChatMember member, ActivityType activityType)
{
final String activityMessage = activityType == ActivityType.JOINED ? " has joined." : " has left.";
final FriendsChatRank rank = member.getRank();
Color textColor = CHAT_FC_TEXT_OPAQUE_BACKGROUND;
Color channelColor = CHAT_FC_NAME_OPAQUE_BACKGROUND;
int rankIcon = -1;
if (client.isResized() && client.getVar(Varbits.TRANSPARENT_CHATBOX) == 1)
{
textColor = CHAT_FC_TEXT_TRANSPARENT_BACKGROUND;
channelColor = CHAT_FC_NAME_TRANSPARENT_BACKGROUND;
}
if (config.chatIcons() && rank != null && rank != FriendsChatRank.UNRANKED)
{
rankIcon = friendChatManager.getIconNumber(rank);
}
ChatMessageBuilder message = new ChatMessageBuilder()
.append("[")
.append(channelColor, friendsChatManager.getName());
if (rankIcon > -1)
{
message
.append(" ")
.img(rankIcon);
}
message
.append("] ")
.append(textColor, member.getName() + activityMessage);
final String messageString = message.build();
client.addChatMessage(ChatMessageType.FRIENDSCHATNOTIFICATION, "", messageString, "");
final ChatLineBuffer chatLineBuffer = client.getChatLineMap().get(ChatMessageType.FRIENDSCHATNOTIFICATION.getType());
final MessageNode[] lines = chatLineBuffer.getLines();
final MessageNode line = lines[0];
MemberJoinMessage joinMessage = new MemberJoinMessage(line, line.getId(), client.getTickCount());
joinMessages.addLast(joinMessage);
}
@Subscribe
public void onVarClientStrChanged(VarClientStrChanged strChanged)
{
if (strChanged.getIndex() == VarClientStr.RECENT_FRIENDS_CHAT.getIndex() && config.recentChats())
{
updateRecentChat(client.getVar(VarClientStr.RECENT_FRIENDS_CHAT));
}
}
@Subscribe
public void onChatMessage(ChatMessage chatMessage)
{
if (client.getGameState() != GameState.LOADING && client.getGameState() != GameState.LOGGED_IN)
{
return;
}
FriendsChatManager friendsChatManager = client.getFriendsChatManager();
if (friendsChatManager == null || friendsChatManager.getCount() == 0)
{
return;
}
switch (chatMessage.getType())
{
case PRIVATECHAT:
case MODPRIVATECHAT:
if (!config.privateMessageIcons())
{
return;
}
break;
case PUBLICCHAT:
case MODCHAT:
if (!config.publicChatIcons())
{
return;
}
break;
case FRIENDSCHAT:
if (!config.chatIcons())
{
return;
}
break;
default:
return;
}
insertRankIcon(chatMessage);
}
@Subscribe
public void onGameStateChanged(GameStateChanged state)
{
GameState gameState = state.getGameState();
if (gameState == GameState.LOGIN_SCREEN || gameState == GameState.CONNECTION_LOST || gameState == GameState.HOPPING)
{
members.clear();
resetCounter();
joinMessages.clear();
}
}
@Subscribe
public void onPlayerSpawned(PlayerSpawned event)
{
final Player local = client.getLocalPlayer();
final Player player = event.getPlayer();
if (player != local && player.isFriendsChatMember())
{
members.add(player);
addCounter();
}
}
@Subscribe
public void onPlayerDespawned(PlayerDespawned event)
{
if (members.remove(event.getPlayer()) && members.isEmpty())
{
resetCounter();
}
}
@Subscribe
public void onFriendsChatChanged(FriendsChatChanged event)
{
if (event.isJoined())
{
joinedTick = client.getTickCount();
}
else
{
members.clear();
resetCounter();
}
activityBuffer.clear();
}
@Subscribe
public void onScriptCallbackEvent(ScriptCallbackEvent scriptCallbackEvent)
{
switch (scriptCallbackEvent.getEventName())
{
case "friendsChatInput":
{
final int[] intStack = client.getIntStack();
final int size = client.getIntStackSize();
// If the user accidentally adds a / when the config and the friends chat chat tab is active, handle it like a normal message
boolean alterDispatch = config.friendsChatTabChat() && !client.getVar(VarClientStr.CHATBOX_TYPED_TEXT).startsWith("/");
intStack[size - 1] = alterDispatch ? 1 : 0;
break;
}
case "confirmFriendsChatKick":
{
if (!config.confirmKicks() || kickConfirmed)
{
break;
}
// Set a flag so the script doesn't instantly kick them
final int[] intStack = client.getIntStack();
final int size = client.getIntStackSize();
intStack[size - 1] = 1;
// Get name of player we are trying to kick
final String[] stringStack = client.getStringStack();
final int stringSize = client.getStringStackSize();
final String kickPlayerName = stringStack[stringSize - 1];
// Show a chatbox panel confirming the kick
clientThread.invokeLater(() -> confirmKickPlayer(kickPlayerName));
break;
}
}
}
@Subscribe
public void onScriptPostFired(ScriptPostFired event)
{
if (event.getScriptId() == ScriptID.FRIENDS_CHAT_CHANNEL_REBUILD && config.showIgnores())
{
colorIgnoredPlayers(config.showIgnoresColor());
}
}
int getMembersSize()
{
return members.size();
}
private void insertRankIcon(final ChatMessage message)
{
final FriendsChatRank rank = friendChatManager.getRank(message.getName());
if (rank != null && rank != FriendsChatRank.UNRANKED)
{
int iconNumber = friendChatManager.getIconNumber(rank);
final String img = "<img=" + iconNumber + ">";
if (message.getType() == ChatMessageType.FRIENDSCHAT)
{
message.getMessageNode()
.setSender(message.getMessageNode().getSender() + " " + img);
}
else
{
message.getMessageNode()
.setName(img + message.getMessageNode().getName());
}
client.refreshChat();
}
}
private void resetChats()
{
Widget chatList = client.getWidget(WidgetInfo.FRIENDS_CHAT_LIST);
Widget chatTitleWidget = client.getWidget(WidgetInfo.FRIENDS_CHAT_TITLE);
if (chatList == null)
{
return;
}
FriendsChatManager friendsChatManager = client.getFriendsChatManager();
if (friendsChatManager == null || friendsChatManager.getCount() == 0)
{
chatList.setChildren(null);
}
chatTitleWidget.setText(TITLE);
}
private void loadFriendsChats()
{
Widget chatList = client.getWidget(WidgetInfo.FRIENDS_CHAT_LIST);
if (chatList == null)
{
return;
}
int y = 2;
chatList.setChildren(null);
for (String chat : Lists.reverse(chats))
{
Widget widget = chatList.createChild(-1, WidgetType.TEXT);
widget.setFontId(494);
widget.setTextColor(0xffffff);
widget.setText(chat);
widget.setOriginalHeight(14);
widget.setOriginalWidth(142);
widget.setOriginalY(y);
widget.setOriginalX(20);
widget.revalidate();
y += 14;
}
}
private void updateRecentChat(String s)
{
if (Strings.isNullOrEmpty(s))
{
return;
}
s = Text.toJagexName(s);
chats.removeIf(s::equalsIgnoreCase);
chats.add(s);
while (chats.size() > MAX_CHATS)
{
chats.remove(0);
}
config.chatsData(Text.toCSV(chats));
}
private void resetCounter()
{
infoBoxManager.removeInfoBox(membersIndicator);
membersIndicator = null;
}
private void addCounter()
{
if (!config.showCounter() || membersIndicator != null || members.isEmpty())
{
return;
}
final BufferedImage image = spriteManager.getSprite(SpriteID.TAB_FRIENDS_CHAT, 0);
membersIndicator = new MembersIndicator(image, this);
infoBoxManager.addInfoBox(membersIndicator);
}
private void confirmKickPlayer(final String kickPlayerName)
{
chatboxPanelManager.openTextMenuInput("Attempting to kick: " + kickPlayerName)
.option("1. Confirm kick", () ->
clientThread.invoke(() ->
{
kickConfirmed = true;
client.runScript(ScriptID.FRIENDS_CHAT_SEND_KICK, kickPlayerName);
kickConfirmed = false;
})
)
.option("2. Cancel", Runnables::doNothing)
.build();
}
private void colorIgnoredPlayers(Color ignoreColor)
{
Widget chatList = client.getWidget(WidgetInfo.FRIENDS_CHAT_LIST);
if (chatList == null || chatList.getChildren() == null)
{
return;
}
NameableContainer<Ignore> ignoreContainer = client.getIgnoreContainer();
// Iterate every 3 widgets, since the order of widgets is name, world, icon
for (int i = 0; i < chatList.getChildren().length; i += 3)
{
Widget listWidget = chatList.getChild(i);
String memberName = listWidget.getText();
if (memberName.isEmpty() || ignoreContainer.findByName(memberName) == null)
{
continue;
}
listWidget.setTextColor(ignoreColor.getRGB());
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, trimbe <github.com/trimbe>
* 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.friendschat;
import lombok.AllArgsConstructor;
import lombok.Value;
import net.runelite.api.FriendsChatMember;
@Value
@AllArgsConstructor
class MemberActivity
{
private ActivityType activityType;
private FriendsChatMember member;
private Integer tick;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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.friendschat;
import lombok.Value;
import net.runelite.api.MessageNode;
@Value
class MemberJoinMessage
{
private final MessageNode messageNode;
private final int getMessageId;
private final int tick;
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2018 Sebastiaan <https://github.com/SebastiaanVanspauwen>
* 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.friendschat;
import java.awt.Color;
import java.awt.image.BufferedImage;
import net.runelite.client.ui.overlay.infobox.Counter;
class MembersIndicator extends Counter
{
private final FriendsChatPlugin plugin;
MembersIndicator(BufferedImage image, FriendsChatPlugin plugin)
{
super(image, plugin, plugin.getMembersSize());
this.plugin = plugin;
}
@Override
public int getCount()
{
return plugin.getMembersSize();
}
@Override
public String getTooltip()
{
return plugin.getMembersSize() + " friends chat member(s) near you";
}
@Override
public Color getTextColor()
{
return Color.WHITE;
}
}

View File

@@ -144,7 +144,7 @@ class SceneUploader
WallObject wallObject = tile.getWallObject();
if (wallObject != null)
{
Renderable renderable1 = wallObject.getRenderable();
Renderable renderable1 = wallObject.getRenderable1();
if (renderable1 instanceof Model)
{
uploadModel((Model) renderable1, vertexBuffer, uvBuffer);

View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* 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.grandexchange;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup(GrandExchangeConfig.CONFIG_GROUP)
public interface GrandExchangeConfig extends Config
{
String CONFIG_GROUP = "grandexchange";
@ConfigItem(
position = 1,
keyName = "quickLookup",
name = "Hotkey lookup (Alt + Left click)",
description = "Configures whether to enable the hotkey lookup for ge searches"
)
default boolean quickLookup()
{
return true;
}
@ConfigItem(
position = 2,
keyName = "enableNotifications",
name = "Enable Notifications",
description = "Configures whether to enable notifications when an offer updates"
)
default boolean enableNotifications()
{
return true;
}
@ConfigItem(
position = 3,
keyName = "enableOsbPrices",
name = "Enable OSB actively traded prices",
description = "Shows the OSBuddy actively traded price at the GE"
)
default boolean enableOsbPrices()
{
return false;
}
@ConfigItem(
position = 4,
keyName = "enableGeLimits",
name = "Enable GE Limits on GE",
description = "Shows the GE Limits on the GE"
)
default boolean enableGELimits()
{
return true;
}
@ConfigItem(
position = 5,
keyName = "enableGELimitReset",
name = "Enable GE Limit Reset Timer",
description = "Shows when GE Trade limits reset (H:MM)"
)
default boolean enableGELimitReset()
{
return true;
}
@ConfigItem(
position = 6,
keyName = "showTotal",
name = "Show grand exchange total",
description = "Show grand exchange total"
)
default boolean showTotal()
{
return true;
}
@ConfigItem(
position = 7,
keyName = "showExact",
name = "Show exact total value",
description = "Show exact total value"
)
default boolean showExact()
{
return false;
}
@ConfigItem(
position = 8,
keyName = "highlightSearchMatch",
name = "Highlight Search Match",
description = "Highlights the search match with an underline"
)
default boolean highlightSearchMatch()
{
return true;
}
@ConfigItem(
position = 9,
keyName = "geSearchMode",
name = "Search Mode",
description = "The search mode to use for the GE<br>"
+ "Default - Matches exact text only<br>"
+ "Fuzzy Only - Matches inexact text such as 'sara sword'<br>"
+ "Fuzzy Fallback - Uses default search, falling back to fuzzy search if no results were found"
)
default GrandExchangeSearchMode geSearchMode()
{
return GrandExchangeSearchMode.DEFAULT;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (c) 2018, Jeremy Plsek <https://github.com/jplsek>
* 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.grandexchange;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.inject.Inject;
import javax.swing.SwingUtilities;
import net.runelite.api.Client;
import net.runelite.api.MenuEntry;
import net.runelite.client.input.KeyListener;
import net.runelite.client.input.MouseAdapter;
import static net.runelite.client.plugins.grandexchange.GrandExchangePlugin.SEARCH_GRAND_EXCHANGE;
import net.runelite.client.util.Text;
public class GrandExchangeInputListener extends MouseAdapter implements KeyListener
{
private final Client client;
private final GrandExchangePlugin plugin;
@Inject
private GrandExchangeInputListener(Client client, GrandExchangePlugin plugin)
{
this.client = client;
this.plugin = plugin;
}
@Override
public MouseEvent mouseClicked(MouseEvent e)
{
// Check if left click + alt
if (e.getButton() == MouseEvent.BUTTON1 && e.isAltDown())
{
final MenuEntry[] menuEntries = client.getMenuEntries();
for (final MenuEntry menuEntry : menuEntries)
{
if (menuEntry.getOption().equals(SEARCH_GRAND_EXCHANGE))
{
search(Text.removeTags(menuEntry.getTarget()));
e.consume();
break;
}
}
}
return super.mouseClicked(e);
}
private void search(final String itemName)
{
SwingUtilities.invokeLater(() ->
{
plugin.getPanel().showSearch();
if (!plugin.getButton().isSelected())
{
plugin.getButton().getOnSelect().run();
}
plugin.getPanel().getSearchPanel().priceLookup(itemName);
});
}
@Override
public void keyTyped(KeyEvent e)
{
}
@Override
public void keyPressed(KeyEvent e)
{
if (e.isAltDown())
{
plugin.setHotKeyPressed(true);
}
}
@Override
public void keyReleased(KeyEvent e)
{
if (!e.isAltDown())
{
plugin.setHotKeyPressed(false);
}
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* Copyright (c) 2018, Psikoi <https://github.com/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.grandexchange;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.util.AsyncBufferedImage;
import net.runelite.client.util.QuantityFormatter;
/**
* This panel displays an individual item result in the
* Grand Exchange search plugin.
*/
class GrandExchangeItemPanel extends JPanel
{
private static final Dimension ICON_SIZE = new Dimension(32, 32);
GrandExchangeItemPanel(AsyncBufferedImage icon, String name, int itemID, int gePrice, Double
haPrice, int geItemLimit)
{
BorderLayout layout = new BorderLayout();
layout.setHgap(5);
setLayout(layout);
setToolTipText(name);
setBackground(ColorScheme.DARKER_GRAY_COLOR);
Color background = getBackground();
List<JPanel> panels = new ArrayList<>();
panels.add(this);
MouseAdapter itemPanelMouseListener = new MouseAdapter()
{
@Override
public void mouseEntered(MouseEvent e)
{
for (JPanel panel : panels)
{
matchComponentBackground(panel, ColorScheme.DARK_GRAY_HOVER_COLOR);
}
setCursor(new Cursor(Cursor.HAND_CURSOR));
}
@Override
public void mouseExited(MouseEvent e)
{
for (JPanel panel : panels)
{
matchComponentBackground(panel, background);
}
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
@Override
public void mouseReleased(MouseEvent e)
{
GrandExchangePlugin.openGeLink(name, itemID);
}
};
addMouseListener(itemPanelMouseListener);
setBorder(new EmptyBorder(5, 5, 5, 0));
// Icon
JLabel itemIcon = new JLabel();
itemIcon.setPreferredSize(ICON_SIZE);
if (icon != null)
{
icon.addTo(itemIcon);
}
add(itemIcon, BorderLayout.LINE_START);
// Item details panel
JPanel rightPanel = new JPanel(new GridLayout(3, 1));
panels.add(rightPanel);
rightPanel.setBackground(background);
// Item name
JLabel itemName = new JLabel();
itemName.setForeground(Color.WHITE);
itemName.setMaximumSize(new Dimension(0, 0)); // to limit the label's size for
itemName.setPreferredSize(new Dimension(0, 0)); // items with longer names
itemName.setText(name);
rightPanel.add(itemName);
// Ge price
JLabel gePriceLabel = new JLabel();
if (gePrice > 0)
{
gePriceLabel.setText(QuantityFormatter.formatNumber(gePrice) + " gp");
}
else
{
gePriceLabel.setText("N/A");
}
gePriceLabel.setForeground(ColorScheme.GRAND_EXCHANGE_PRICE);
rightPanel.add(gePriceLabel);
JPanel alchAndLimitPanel = new JPanel(new BorderLayout());
panels.add(alchAndLimitPanel);
alchAndLimitPanel.setBackground(background);
// Alch price
JLabel haPriceLabel = new JLabel();
haPriceLabel.setText(QuantityFormatter.formatNumber(haPrice.intValue()) + " alch");
haPriceLabel.setForeground(ColorScheme.GRAND_EXCHANGE_ALCH);
alchAndLimitPanel.add(haPriceLabel, BorderLayout.WEST);
// GE Limit
JLabel geLimitLabel = new JLabel();
String limitLabelText = geItemLimit == 0 ? "" : "Limit " + QuantityFormatter.formatNumber(geItemLimit);
geLimitLabel.setText(limitLabelText);
geLimitLabel.setForeground(ColorScheme.GRAND_EXCHANGE_LIMIT);
geLimitLabel.setBorder(new CompoundBorder(geLimitLabel.getBorder(), new EmptyBorder(0, 0, 0, 7)));
alchAndLimitPanel.add(geLimitLabel, BorderLayout.EAST);
rightPanel.add(alchAndLimitPanel);
add(rightPanel, BorderLayout.CENTER);
}
private void matchComponentBackground(JPanel panel, Color color)
{
panel.setBackground(color);
for (Component c : panel.getComponents())
{
c.setBackground(color);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* 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.grandexchange;
import lombok.Value;
import net.runelite.client.util.AsyncBufferedImage;
@Value
class GrandExchangeItems
{
private final AsyncBufferedImage icon;
private final String name;
private final int itemId;
private final int gePrice;
private final double haPrice;
private final int geItemLimit;
}

View File

@@ -0,0 +1,289 @@
/*
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2018, Psikoi <https://github.com/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.grandexchange;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import javax.annotation.Nullable;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import net.runelite.api.GrandExchangeOffer;
import static net.runelite.api.GrandExchangeOfferState.BOUGHT;
import static net.runelite.api.GrandExchangeOfferState.BUYING;
import static net.runelite.api.GrandExchangeOfferState.CANCELLED_BUY;
import static net.runelite.api.GrandExchangeOfferState.CANCELLED_SELL;
import static net.runelite.api.GrandExchangeOfferState.EMPTY;
import net.runelite.api.ItemComposition;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.components.ThinProgressBar;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.QuantityFormatter;
public class GrandExchangeOfferSlot extends JPanel
{
private static final String FACE_CARD = "FACE_CARD";
private static final String DETAILS_CARD = "DETAILS_CARD";
private static final ImageIcon RIGHT_ARROW_ICON;
private static final ImageIcon LEFT_ARROW_ICON;
private final JPanel container = new JPanel();
private final CardLayout cardLayout = new CardLayout();
private final JLabel itemIcon = new JLabel();
private final JLabel itemName = new JLabel();
private final JLabel offerInfo = new JLabel();
private final JLabel itemPrice = new JLabel();
private final JLabel offerSpent = new JLabel();
private final ThinProgressBar progressBar = new ThinProgressBar();
private boolean showingFace = true;
static
{
final BufferedImage rightArrow = ImageUtil.alphaOffset(ImageUtil.getResourceStreamFromClass(GrandExchangeOfferSlot.class, "/util/arrow_right.png"), 0.25f);
RIGHT_ARROW_ICON = new ImageIcon(rightArrow);
LEFT_ARROW_ICON = new ImageIcon(ImageUtil.flipImage(rightArrow, true, false));
}
/**
* This (sub)panel is used for each GE slot displayed
* in the sidebar
*/
GrandExchangeOfferSlot()
{
setLayout(new BorderLayout());
setBackground(ColorScheme.DARK_GRAY_COLOR);
setBorder(new EmptyBorder(7, 0, 0, 0));
final MouseListener ml = new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent mouseEvent)
{
if (SwingUtilities.isLeftMouseButton(mouseEvent))
{
switchPanel();
}
}
@Override
public void mouseEntered(MouseEvent mouseEvent)
{
container.setBackground(ColorScheme.DARKER_GRAY_HOVER_COLOR);
}
@Override
public void mouseExited(MouseEvent mouseEvent)
{
container.setBackground(ColorScheme.DARKER_GRAY_COLOR);
}
};
container.setLayout(cardLayout);
container.setBackground(ColorScheme.DARKER_GRAY_COLOR);
JPanel faceCard = new JPanel();
faceCard.setBackground(ColorScheme.DARKER_GRAY_COLOR);
faceCard.setLayout(new BorderLayout());
faceCard.addMouseListener(ml);
itemIcon.setVerticalAlignment(JLabel.CENTER);
itemIcon.setHorizontalAlignment(JLabel.CENTER);
itemIcon.setPreferredSize(new Dimension(45, 45));
itemName.setForeground(Color.WHITE);
itemName.setVerticalAlignment(JLabel.BOTTOM);
itemName.setFont(FontManager.getRunescapeSmallFont());
offerInfo.setForeground(ColorScheme.LIGHT_GRAY_COLOR);
offerInfo.setVerticalAlignment(JLabel.TOP);
offerInfo.setFont(FontManager.getRunescapeSmallFont());
JLabel switchFaceViewIcon = new JLabel();
switchFaceViewIcon.setIcon(RIGHT_ARROW_ICON);
switchFaceViewIcon.setVerticalAlignment(JLabel.CENTER);
switchFaceViewIcon.setHorizontalAlignment(JLabel.CENTER);
switchFaceViewIcon.setPreferredSize(new Dimension(30, 45));
JPanel offerFaceDetails = new JPanel();
offerFaceDetails.setBackground(ColorScheme.DARKER_GRAY_COLOR);
offerFaceDetails.setLayout(new GridLayout(2, 1, 0, 2));
offerFaceDetails.add(itemName);
offerFaceDetails.add(offerInfo);
faceCard.add(offerFaceDetails, BorderLayout.CENTER);
faceCard.add(itemIcon, BorderLayout.WEST);
faceCard.add(switchFaceViewIcon, BorderLayout.EAST);
JPanel detailsCard = new JPanel();
detailsCard.setBackground(ColorScheme.DARKER_GRAY_COLOR);
detailsCard.setLayout(new BorderLayout());
detailsCard.setBorder(new EmptyBorder(0, 15, 0, 0));
detailsCard.addMouseListener(ml);
itemPrice.setForeground(Color.WHITE);
itemPrice.setVerticalAlignment(JLabel.BOTTOM);
itemPrice.setFont(FontManager.getRunescapeSmallFont());
offerSpent.setForeground(Color.WHITE);
offerSpent.setVerticalAlignment(JLabel.TOP);
offerSpent.setFont(FontManager.getRunescapeSmallFont());
JLabel switchDetailsViewIcon = new JLabel();
switchDetailsViewIcon.setIcon(LEFT_ARROW_ICON);
switchDetailsViewIcon.setVerticalAlignment(JLabel.CENTER);
switchDetailsViewIcon.setHorizontalAlignment(JLabel.CENTER);
switchDetailsViewIcon.setPreferredSize(new Dimension(30, 45));
JPanel offerDetails = new JPanel();
offerDetails.setBackground(ColorScheme.DARKER_GRAY_COLOR);
offerDetails.setLayout(new GridLayout(2, 1));
offerDetails.add(itemPrice);
offerDetails.add(offerSpent);
detailsCard.add(offerDetails, BorderLayout.CENTER);
detailsCard.add(switchDetailsViewIcon, BorderLayout.EAST);
container.add(faceCard, FACE_CARD);
container.add(detailsCard, DETAILS_CARD);
cardLayout.show(container, FACE_CARD);
add(container, BorderLayout.CENTER);
add(progressBar, BorderLayout.SOUTH);
}
void updateOffer(ItemComposition offerItem, BufferedImage itemImage, @Nullable GrandExchangeOffer newOffer)
{
if (newOffer == null || newOffer.getState() == EMPTY)
{
return;
}
else
{
cardLayout.show(container, FACE_CARD);
itemName.setText(offerItem.getName());
itemIcon.setIcon(new ImageIcon(itemImage));
boolean buying = newOffer.getState() == BOUGHT
|| newOffer.getState() == BUYING
|| newOffer.getState() == CANCELLED_BUY;
String offerState = (buying ? "Bought " : "Sold ")
+ QuantityFormatter.quantityToRSDecimalStack(newOffer.getQuantitySold()) + " / "
+ QuantityFormatter.quantityToRSDecimalStack(newOffer.getTotalQuantity());
offerInfo.setText(offerState);
itemPrice.setText(htmlLabel("Price each: ", QuantityFormatter.formatNumber(newOffer.getPrice())));
String action = buying ? "Spent: " : "Received: ";
offerSpent.setText(htmlLabel(action, QuantityFormatter.formatNumber(newOffer.getSpent()) + " / "
+ QuantityFormatter.formatNumber(newOffer.getPrice() * newOffer.getTotalQuantity())));
progressBar.setForeground(getProgressColor(newOffer));
progressBar.setMaximumValue(newOffer.getTotalQuantity());
progressBar.setValue(newOffer.getQuantitySold());
final JPopupMenu popupMenu = new JPopupMenu();
popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5));
final JMenuItem openGeLink = new JMenuItem("Open Grand Exchange website");
openGeLink.addActionListener(e -> GrandExchangePlugin.openGeLink(offerItem.getName(), offerItem.getId()));
popupMenu.add(openGeLink);
/* Couldn't set the tooltip for the container panel as the children override it, so I'm setting
* the tooltips on the children instead. */
for (Component c : container.getComponents())
{
if (c instanceof JPanel)
{
JPanel panel = (JPanel) c;
panel.setToolTipText(htmlTooltip(((int) progressBar.getPercentage()) + "%"));
panel.setComponentPopupMenu(popupMenu);
}
}
}
revalidate();
repaint();
}
private String htmlTooltip(String value)
{
return "<html><body style = 'color:" + ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR) + "'>Progress: <span style = 'color:white'>" + value + "</span></body></html>";
}
private String htmlLabel(String key, String value)
{
return "<html><body style = 'color:white'>" + key + "<span style = 'color:" + ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR) + "'>" + value + "</span></body></html>";
}
private void switchPanel()
{
this.showingFace = !this.showingFace;
cardLayout.show(container, showingFace ? FACE_CARD : DETAILS_CARD);
}
private Color getProgressColor(GrandExchangeOffer offer)
{
if (offer.getState() == CANCELLED_BUY || offer.getState() == CANCELLED_SELL)
{
return ColorScheme.PROGRESS_ERROR_COLOR;
}
if (offer.getQuantitySold() == offer.getTotalQuantity())
{
return ColorScheme.PROGRESS_COMPLETE_COLOR;
}
return ColorScheme.PROGRESS_INPROGRESS_COLOR;
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2018, Psikoi <https://github.com/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.grandexchange;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.image.BufferedImage;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import net.runelite.api.GrandExchangeOffer;
import net.runelite.api.GrandExchangeOfferState;
import net.runelite.api.ItemComposition;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.components.PluginErrorPanel;
public class GrandExchangeOffersPanel extends JPanel
{
private static final String ERROR_PANEL = "ERROR_PANEL";
private static final String OFFERS_PANEL = "OFFERS_PANEL";
private static final int MAX_OFFERS = 8;
private final GridBagConstraints constraints = new GridBagConstraints();
private final CardLayout cardLayout = new CardLayout();
/* The offers container, this will hold all the individual ge offers panels */
private final JPanel offerPanel = new JPanel();
/* The center panel, this holds either the error panel or the offers container */
private final JPanel container = new JPanel(cardLayout);
private final GrandExchangeOfferSlot[] offerSlotPanels = new GrandExchangeOfferSlot[MAX_OFFERS];
GrandExchangeOffersPanel()
{
setLayout(new BorderLayout());
setBackground(ColorScheme.DARK_GRAY_COLOR);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1;
constraints.gridx = 0;
constraints.gridy = 0;
/* This panel wraps the offers panel and limits its height */
JPanel offersWrapper = new JPanel(new BorderLayout());
offersWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR);
offersWrapper.add(offerPanel, BorderLayout.NORTH);
offerPanel.setLayout(new GridBagLayout());
offerPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
offerPanel.setBackground(ColorScheme.DARK_GRAY_COLOR);
/* This panel wraps the error panel and limits its height */
JPanel errorWrapper = new JPanel(new BorderLayout());
errorWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR);
/* The error panel, this displays an error message */
PluginErrorPanel errorPanel = new PluginErrorPanel();
errorWrapper.add(errorPanel, BorderLayout.NORTH);
errorPanel.setBorder(new EmptyBorder(50, 20, 20, 20));
errorPanel.setContent("No offers detected", "No grand exchange offers were found on your account.");
container.add(offersWrapper, OFFERS_PANEL);
container.add(errorWrapper, ERROR_PANEL);
add(container, BorderLayout.CENTER);
resetOffers();
}
void resetOffers()
{
offerPanel.removeAll();
for (int i = 0; i < offerSlotPanels.length; i++)
{
offerSlotPanels[i] = null;
}
updateEmptyOffersPanel();
}
void updateOffer(ItemComposition item, BufferedImage itemImage, GrandExchangeOffer newOffer, int slot)
{
/* If slot was previously filled, and is now empty, remove it from the list */
if (newOffer == null || newOffer.getState() == GrandExchangeOfferState.EMPTY)
{
if (offerSlotPanels[slot] != null)
{
offerPanel.remove(offerSlotPanels[slot]);
offerSlotPanels[slot] = null;
revalidate();
repaint();
}
removeTopMargin();
updateEmptyOffersPanel();
return;
}
/* If slot was empty, and is now filled, add it to the list */
if (offerSlotPanels[slot] == null)
{
GrandExchangeOfferSlot newSlot = new GrandExchangeOfferSlot();
offerSlotPanels[slot] = newSlot;
offerPanel.add(newSlot, constraints);
constraints.gridy++;
}
offerSlotPanels[slot].updateOffer(item, itemImage, newOffer);
removeTopMargin();
revalidate();
repaint();
updateEmptyOffersPanel();
}
/**
* Reset the border for the first offer slot.
*/
private void removeTopMargin()
{
if (offerPanel.getComponentCount() <= 0)
{
return;
}
JPanel firstItem = (JPanel) offerPanel.getComponent(0);
firstItem.setBorder(null);
}
/**
* This method calculates the amount of empty ge offer slots, if all slots are empty,
* it shows the error panel.
*/
private void updateEmptyOffersPanel()
{
int nullCount = 0;
for (GrandExchangeOfferSlot slot : offerSlotPanels)
{
if (slot == null)
{
nullCount++;
}
}
if (nullCount == MAX_OFFERS)
{
offerPanel.removeAll();
cardLayout.show(container, ERROR_PANEL);
}
else
{
cardLayout.show(container, OFFERS_PANEL);
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2018, Psikoi <https://github.com/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.grandexchange;
import java.awt.BorderLayout;
import java.util.concurrent.ScheduledExecutorService;
import javax.inject.Inject;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import lombok.Getter;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.game.ItemManager;
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;
class GrandExchangePanel extends PluginPanel
{
// this panel will hold either the ge search panel or the ge offers panel
private final JPanel display = new JPanel();
private final MaterialTabGroup tabGroup = new MaterialTabGroup(display);
private final MaterialTab searchTab;
@Getter
private GrandExchangeSearchPanel searchPanel;
@Getter
private GrandExchangeOffersPanel offersPanel;
@Inject
private GrandExchangePanel(ClientThread clientThread, ItemManager itemManager, ScheduledExecutorService executor)
{
super(false);
setLayout(new BorderLayout());
setBackground(ColorScheme.DARK_GRAY_COLOR);
// Search Panel
searchPanel = new GrandExchangeSearchPanel(clientThread, itemManager, executor);
//Offers Panel
offersPanel = new GrandExchangeOffersPanel();
MaterialTab offersTab = new MaterialTab("Offers", tabGroup, offersPanel);
searchTab = new MaterialTab("Search", tabGroup, searchPanel);
tabGroup.setBorder(new EmptyBorder(5, 0, 0, 0));
tabGroup.addTab(offersTab);
tabGroup.addTab(searchTab);
tabGroup.select(offersTab); // selects the default selected tab
add(tabGroup, BorderLayout.NORTH);
add(display, BorderLayout.CENTER);
}
void showSearch()
{
if (searchPanel.isShowing())
{
return;
}
tabGroup.select(searchTab);
revalidate();
}
}

View File

@@ -0,0 +1,964 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* Copyright (c) 2017, Robbie <https://github.com/rbbi>
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2020, Dennis <me@dennis.dev>
* 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.grandexchange;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Shorts;
import com.google.gson.Gson;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.inject.Inject;
import javax.swing.SwingUtilities;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.GrandExchangeOffer;
import net.runelite.api.GrandExchangeOfferState;
import net.runelite.api.ItemComposition;
import net.runelite.api.MenuAction;
import net.runelite.api.MenuEntry;
import net.runelite.api.ScriptID;
import net.runelite.api.VarClientStr;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.FocusChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GrandExchangeOfferChanged;
import net.runelite.api.events.GrandExchangeSearched;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.Notifier;
import net.runelite.client.account.AccountSession;
import net.runelite.client.account.SessionManager;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.SessionClose;
import net.runelite.client.events.SessionOpen;
import net.runelite.client.game.ItemManager;
import net.runelite.client.input.KeyManager;
import net.runelite.client.input.MouseManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.LinkBrowser;
import net.runelite.client.util.OSType;
import net.runelite.client.util.QuantityFormatter;
import net.runelite.client.util.Text;
import net.runelite.http.api.ge.GrandExchangeClient;
import net.runelite.http.api.ge.GrandExchangeTrade;
import net.runelite.http.api.item.ItemStats;
import net.runelite.http.api.osbuddy.OSBGrandExchangeClient;
import net.runelite.http.api.osbuddy.OSBGrandExchangeResult;
import net.runelite.http.api.worlds.WorldType;
import okhttp3.OkHttpClient;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.apache.commons.text.similarity.FuzzyScore;
@PluginDescriptor(
name = "Grand Exchange",
description = "Provide additional and/or easier access to Grand Exchange information",
tags = {"external", "integration", "notifications", "panel", "prices", "trade"}
)
@Slf4j
public class GrandExchangePlugin extends Plugin
{
private static final int GE_SLOTS = 8;
private static final int OFFER_CONTAINER_ITEM = 21;
private static final int OFFER_DEFAULT_ITEM_ID = 6512;
private static final String OSB_GE_TEXT = "<br>OSBuddy Actively traded price: ";
private static final String BUY_LIMIT_GE_TEXT = "<br>Buy limit: ";
private static final String BUY_LIMIT_KEY = "buylimit";
private static final Gson GSON = new Gson();
private static final Duration BUY_LIMIT_RESET = Duration.ofHours(4);
static final String SEARCH_GRAND_EXCHANGE = "Search Grand Exchange";
private static final int MAX_RESULT_COUNT = 250;
private static final FuzzyScore FUZZY = new FuzzyScore(Locale.ENGLISH);
private static final Color FUZZY_HIGHLIGHT_COLOR = new Color(0x800000);
@Getter(AccessLevel.PACKAGE)
private NavigationButton button;
@Getter(AccessLevel.PACKAGE)
private GrandExchangePanel panel;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private boolean hotKeyPressed;
@Inject
private GrandExchangeInputListener inputListener;
@Inject
private ItemManager itemManager;
@Inject
private MouseManager mouseManager;
@Inject
private KeyManager keyManager;
@Inject
private Client client;
@Inject
private ClientToolbar clientToolbar;
@Inject
private GrandExchangeConfig config;
@Inject
private Notifier notifier;
@Inject
private ScheduledExecutorService executorService;
@Inject
private SessionManager sessionManager;
@Inject
private ConfigManager configManager;
private Widget grandExchangeText;
private Widget grandExchangeItem;
private String grandExchangeExamine;
private int osbItem;
private OSBGrandExchangeResult osbGrandExchangeResult;
@Inject
private GrandExchangeClient grandExchangeClient;
private boolean loginBurstGeUpdates;
@Inject
private OSBGrandExchangeClient osbGrandExchangeClient;
private boolean wasFuzzySearch;
private String machineUuid;
private String lastUsername;
/**
* Logic from {@link org.apache.commons.text.similarity.FuzzyScore}
*/
@VisibleForTesting
static List<Integer> findFuzzyIndices(String term, String query)
{
List<Integer> indices = new ArrayList<>();
// fuzzy logic is case insensitive. We normalize the Strings to lower
// case right from the start. Turning characters to lower case
// via Character.toLowerCase(char) is unfortunately insufficient
// as it does not accept a locale.
final String termLowerCase = term.toLowerCase();
final String queryLowerCase = query.toLowerCase();
// the position in the term which will be scanned next for potential
// query character matches
int termIndex = 0;
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++)
{
final char queryChar = queryLowerCase.charAt(queryIndex);
boolean termCharacterMatchFound = false;
for (; termIndex < termLowerCase.length()
&& !termCharacterMatchFound; termIndex++)
{
final char termChar = termLowerCase.charAt(termIndex);
if (queryChar == termChar)
{
indices.add(termIndex);
// we can leave the nested loop. Every character in the
// query can match at most one character in the term.
termCharacterMatchFound = true;
}
}
}
return indices;
}
private SavedOffer getOffer(int slot)
{
String offer = configManager.getRSProfileConfiguration("geoffer", Integer.toString(slot));
if (offer == null)
{
return null;
}
return GSON.fromJson(offer, SavedOffer.class);
}
private void setOffer(int slot, SavedOffer offer)
{
configManager.setRSProfileConfiguration("geoffer", Integer.toString(slot), GSON.toJson(offer));
}
private void deleteOffer(int slot)
{
configManager.unsetRSProfileConfiguration("geoffer", Integer.toString(slot));
}
@Provides
GrandExchangeConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(GrandExchangeConfig.class);
}
@Provides
OSBGrandExchangeClient provideOsbGrandExchangeClient(OkHttpClient okHttpClient)
{
return new OSBGrandExchangeClient(okHttpClient);
}
@Provides
GrandExchangeClient provideGrandExchangeClient(OkHttpClient okHttpClient)
{
return new GrandExchangeClient(okHttpClient);
}
@Override
protected void startUp()
{
panel = injector.getInstance(GrandExchangePanel.class);
final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "ge_icon.png");
button = NavigationButton.builder()
.tooltip("Grand Exchange")
.icon(icon)
.priority(3)
.panel(panel)
.build();
clientToolbar.addNavigation(button);
if (config.quickLookup())
{
mouseManager.registerMouseListener(inputListener);
keyManager.registerKeyListener(inputListener);
}
AccountSession accountSession = sessionManager.getAccountSession();
if (accountSession != null)
{
grandExchangeClient.setUuid(accountSession.getUuid());
}
else
{
grandExchangeClient.setUuid(null);
}
osbItem = -1;
osbGrandExchangeResult = null;
}
@Override
protected void shutDown()
{
clientToolbar.removeNavigation(button);
mouseManager.unregisterMouseListener(inputListener);
keyManager.unregisterKeyListener(inputListener);
grandExchangeText = null;
grandExchangeItem = null;
lastUsername = machineUuid = null;
}
@Subscribe
public void onSessionOpen(SessionOpen sessionOpen)
{
AccountSession accountSession = sessionManager.getAccountSession();
grandExchangeClient.setUuid(accountSession.getUuid());
}
@Subscribe
public void onSessionClose(SessionClose sessionClose)
{
grandExchangeClient.setUuid(null);
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals(GrandExchangeConfig.CONFIG_GROUP))
{
if (event.getKey().equals("quickLookup"))
{
if (config.quickLookup())
{
mouseManager.registerMouseListener(inputListener);
keyManager.registerKeyListener(inputListener);
}
else
{
mouseManager.unregisterMouseListener(inputListener);
keyManager.unregisterKeyListener(inputListener);
}
}
}
}
@Subscribe
public void onGrandExchangeOfferChanged(GrandExchangeOfferChanged offerEvent)
{
final int slot = offerEvent.getSlot();
final GrandExchangeOffer offer = offerEvent.getOffer();
if (offer.getState() == GrandExchangeOfferState.EMPTY && client.getGameState() != GameState.LOGGED_IN)
{
// Trades are cleared by the client during LOGIN_SCREEN/HOPPING/LOGGING_IN, ignore those so we don't
// zero and re-submit the trade on login as an update
return;
}
log.debug("GE offer updated: state: {}, slot: {}, item: {}, qty: {}, login: {}",
offer.getState(), slot, offer.getItemId(), offer.getQuantitySold(), loginBurstGeUpdates);
ItemComposition offerItem = itemManager.getItemComposition(offer.getItemId());
boolean shouldStack = offerItem.isStackable() || offer.getTotalQuantity() > 1;
BufferedImage itemImage = itemManager.getImage(offer.getItemId(), offer.getTotalQuantity(), shouldStack);
SwingUtilities.invokeLater(() -> panel.getOffersPanel().updateOffer(offerItem, itemImage, offer, slot));
submitTrade(slot, offer);
updateConfig(slot, offer);
if (loginBurstGeUpdates && slot == GE_SLOTS - 1) // slots are sent sequentially on login; this is the last one
{
loginBurstGeUpdates = false;
}
}
@VisibleForTesting
void submitTrade(int slot, GrandExchangeOffer offer)
{
GrandExchangeOfferState state = offer.getState();
if (state != GrandExchangeOfferState.CANCELLED_BUY && state != GrandExchangeOfferState.CANCELLED_SELL && state != GrandExchangeOfferState.BUYING && state != GrandExchangeOfferState.SELLING)
{
return;
}
SavedOffer savedOffer = getOffer(slot);
if (savedOffer == null && (state == GrandExchangeOfferState.BUYING || state == GrandExchangeOfferState.SELLING) && offer.getQuantitySold() == 0)
{
// new offer
GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade();
grandExchangeTrade.setBuy(state == GrandExchangeOfferState.BUYING);
grandExchangeTrade.setItemId(offer.getItemId());
grandExchangeTrade.setTotal(offer.getTotalQuantity());
grandExchangeTrade.setOffer(offer.getPrice());
grandExchangeTrade.setSlot(slot);
grandExchangeTrade.setWorldType(getGeWorldType());
grandExchangeTrade.setLogin(loginBurstGeUpdates);
log.debug("Submitting new trade: {}", grandExchangeTrade);
grandExchangeClient.submit(grandExchangeTrade);
return;
}
if (savedOffer == null || savedOffer.getItemId() != offer.getItemId() || savedOffer.getPrice() != offer.getPrice() || savedOffer.getTotalQuantity() != offer.getTotalQuantity())
{
// desync
return;
}
if (savedOffer.getState() == offer.getState() && savedOffer.getQuantitySold() == offer.getQuantitySold())
{
// no change
return;
}
if (state == GrandExchangeOfferState.CANCELLED_BUY || state == GrandExchangeOfferState.CANCELLED_SELL)
{
GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade();
grandExchangeTrade.setBuy(state == GrandExchangeOfferState.CANCELLED_BUY);
grandExchangeTrade.setCancel(true);
grandExchangeTrade.setItemId(offer.getItemId());
grandExchangeTrade.setQty(offer.getQuantitySold());
grandExchangeTrade.setTotal(offer.getTotalQuantity());
grandExchangeTrade.setSpent(offer.getSpent());
grandExchangeTrade.setOffer(offer.getPrice());
grandExchangeTrade.setSlot(slot);
grandExchangeTrade.setWorldType(getGeWorldType());
grandExchangeTrade.setLogin(loginBurstGeUpdates);
log.debug("Submitting cancelled: {}", grandExchangeTrade);
grandExchangeClient.submit(grandExchangeTrade);
return;
}
final int qty = offer.getQuantitySold() - savedOffer.getQuantitySold();
final int dspent = offer.getSpent() - savedOffer.getSpent();
if (qty <= 0 || dspent <= 0)
{
return;
}
GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade();
grandExchangeTrade.setBuy(state == GrandExchangeOfferState.BUYING);
grandExchangeTrade.setItemId(offer.getItemId());
grandExchangeTrade.setQty(offer.getQuantitySold());
grandExchangeTrade.setDqty(qty);
grandExchangeTrade.setTotal(offer.getTotalQuantity());
grandExchangeTrade.setDspent(dspent);
grandExchangeTrade.setSpent(offer.getSpent());
grandExchangeTrade.setOffer(offer.getPrice());
grandExchangeTrade.setSlot(slot);
grandExchangeTrade.setWorldType(getGeWorldType());
grandExchangeTrade.setLogin(loginBurstGeUpdates);
log.debug("Submitting trade: {}", grandExchangeTrade);
grandExchangeClient.submit(grandExchangeTrade);
}
private WorldType getGeWorldType()
{
EnumSet<net.runelite.api.WorldType> worldTypes = client.getWorldType();
if (worldTypes.contains(net.runelite.api.WorldType.DEADMAN))
{
return WorldType.DEADMAN;
}
else if (worldTypes.contains(net.runelite.api.WorldType.DEADMAN_TOURNAMENT))
{
return WorldType.DEADMAN_TOURNAMENT;
}
else
{
return null;
}
}
private void updateConfig(int slot, GrandExchangeOffer offer)
{
if (offer.getState() == GrandExchangeOfferState.EMPTY)
{
deleteOffer(slot);
}
else
{
SavedOffer savedOffer = new SavedOffer();
savedOffer.setItemId(offer.getItemId());
savedOffer.setQuantitySold(offer.getQuantitySold());
savedOffer.setTotalQuantity(offer.getTotalQuantity());
savedOffer.setPrice(offer.getPrice());
savedOffer.setSpent(offer.getSpent());
savedOffer.setState(offer.getState());
setOffer(slot, savedOffer);
updateLimitTimer(offer);
}
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
if (!this.config.enableNotifications() || event.getType() != ChatMessageType.GAMEMESSAGE)
{
return;
}
String message = Text.removeTags(event.getMessage());
if (message.startsWith("Grand Exchange:"))
{
this.notifier.notify(message);
}
}
@Subscribe
public void onGameStateChanged(GameStateChanged gameStateChanged)
{
if (gameStateChanged.getGameState() == GameState.LOGIN_SCREEN)
{
panel.getOffersPanel().resetOffers();
loginBurstGeUpdates = true;
}
else if (gameStateChanged.getGameState() == GameState.LOGGED_IN)
{
grandExchangeClient.setMachineId(getMachineUuid());
}
}
@Subscribe
public void onMenuEntryAdded(MenuEntryAdded event)
{
// At the moment, if the user disables quick lookup, the input listener gets disabled. Thus, isHotKeyPressed()
// should always return false when quick lookup is disabled.
// Replace the default option with "Search ..." when holding alt
if (client.getGameState() != GameState.LOGGED_IN || !hotKeyPressed)
{
return;
}
final MenuEntry[] entries = client.getMenuEntries();
final MenuEntry menuEntry = entries[entries.length - 1];
final int widgetId = menuEntry.getParam1();
final int groupId = WidgetInfo.TO_GROUP(widgetId);
switch (groupId)
{
case WidgetID.BANK_GROUP_ID:
// Don't show for view tabs and such
if (WidgetInfo.TO_CHILD(widgetId) != WidgetInfo.BANK_ITEM_CONTAINER.getChildId())
{
break;
}
case WidgetID.INVENTORY_GROUP_ID:
case WidgetID.BANK_INVENTORY_GROUP_ID:
case WidgetID.GRAND_EXCHANGE_INVENTORY_GROUP_ID:
case WidgetID.SHOP_INVENTORY_GROUP_ID:
menuEntry.setOption(SEARCH_GRAND_EXCHANGE);
menuEntry.setType(MenuAction.RUNELITE.getId());
client.setMenuEntries(entries);
}
}
@Subscribe
public void onFocusChanged(FocusChanged focusChanged)
{
if (!focusChanged.isFocused())
{
setHotKeyPressed(false);
}
}
@Subscribe
public void onWidgetLoaded(WidgetLoaded event)
{
switch (event.getGroupId())
{
// Grand exchange was opened.
case WidgetID.GRAND_EXCHANGE_GROUP_ID:
Widget grandExchangeOffer = client.getWidget(WidgetInfo.GRAND_EXCHANGE_OFFER_CONTAINER);
grandExchangeText = client.getWidget(WidgetInfo.GRAND_EXCHANGE_OFFER_TEXT);
grandExchangeItem = grandExchangeOffer.getChild(OFFER_CONTAINER_ITEM);
break;
// Grand exchange was closed (if it was open before).
case WidgetID.INVENTORY_GROUP_ID:
grandExchangeText = null;
grandExchangeItem = null;
break;
}
}
@Subscribe
public void onScriptPostFired(ScriptPostFired event)
{
// GE offers setup init
if (event.getScriptId() == ScriptID.GE_OFFERS_SETUP_BUILD)
{
rebuildGeText();
}
else if (event.getScriptId() == ScriptID.GE_ITEM_SEARCH && config.highlightSearchMatch())
{
highlightSearchMatches();
}
}
private void highlightSearchMatches()
{
if (!wasFuzzySearch)
{
return;
}
String input = client.getVar(VarClientStr.INPUT_TEXT);
String underlineTag = "<u=" + ColorUtil.colorToHexCode(FUZZY_HIGHLIGHT_COLOR) + ">";
Widget results = client.getWidget(WidgetInfo.CHATBOX_GE_SEARCH_RESULTS);
Widget[] children = results.getDynamicChildren();
int resultCount = children.length / 3;
for (int i = 0; i < resultCount; i++)
{
Widget itemNameWidget = children[i * 3 + 1];
String itemName = itemNameWidget.getText();
List<Integer> indices;
String otherName = itemName.replace('-', ' ');
if (!itemName.contains("-") || FUZZY.fuzzyScore(itemName, input) >= FUZZY.fuzzyScore(otherName, input))
{
indices = findFuzzyIndices(itemName, input);
}
else
{
indices = findFuzzyIndices(otherName, input);
}
Collections.reverse(indices);
StringBuilder newItemName = new StringBuilder(itemName);
for (int index : indices)
{
if (itemName.charAt(index) == ' ' || itemName.charAt(index) == '-')
{
continue;
}
newItemName.insert(index + 1, "</u>");
newItemName.insert(index, underlineTag);
}
itemNameWidget.setText(newItemName.toString());
}
}
@Subscribe
public void onGrandExchangeSearched(GrandExchangeSearched event)
{
wasFuzzySearch = false;
GrandExchangeSearchMode searchMode = config.geSearchMode();
final String input = client.getVar(VarClientStr.INPUT_TEXT);
if (searchMode == GrandExchangeSearchMode.DEFAULT || input.isEmpty())
{
return;
}
event.consume();
client.setGeSearchResultIndex(0);
int resultCount = 0;
if (searchMode == GrandExchangeSearchMode.FUZZY_FALLBACK)
{
List<Integer> ids = IntStream.range(0, client.getItemCount())
.mapToObj(itemManager::getItemComposition)
.filter(item -> item.isTradeable() && item.getNote() == -1
&& item.getName().toLowerCase().contains(input))
.limit(MAX_RESULT_COUNT + 1)
.sorted(Comparator.comparing(ItemComposition::getName))
.map(ItemComposition::getId)
.collect(Collectors.toList());
if (ids.size() > MAX_RESULT_COUNT)
{
client.setGeSearchResultCount(-1);
client.setGeSearchResultIds(null);
}
else
{
resultCount = ids.size();
client.setGeSearchResultCount(resultCount);
client.setGeSearchResultIds(Shorts.toArray(ids));
}
}
if (resultCount == 0)
{
// We do this so that for example the items "Anti-venom ..." are still at the top
// when searching "anti venom"
ToIntFunction<ItemComposition> getScore = item ->
{
int score = FUZZY.fuzzyScore(item.getName(), input);
if (item.getName().contains("-"))
{
return Math.max(FUZZY.fuzzyScore(item.getName().replace('-', ' '), input), score);
}
return score;
};
List<Integer> ids = IntStream.range(0, client.getItemCount())
.mapToObj(itemManager::getItemComposition)
.filter(item -> item.isTradeable() && item.getNote() == -1)
.filter(item -> getScore.applyAsInt(item) > 0)
.sorted(Comparator.comparingInt(getScore).reversed()
.thenComparing(ItemComposition::getName))
.limit(MAX_RESULT_COUNT)
.map(ItemComposition::getId)
.collect(Collectors.toList());
client.setGeSearchResultCount(ids.size());
client.setGeSearchResultIds(Shorts.toArray(ids));
wasFuzzySearch = true;
}
}
@Subscribe
public void onScriptCallbackEvent(ScriptCallbackEvent event)
{
if (!event.getEventName().equals("setGETitle") || !config.showTotal())
{
return;
}
long total = 0;
GrandExchangeOffer[] offers = client.getGrandExchangeOffers();
for (GrandExchangeOffer offer : offers)
{
if (offer != null)
{
total += offer.getPrice() * offer.getTotalQuantity();
}
}
if (total == 0L)
{
return;
}
StringBuilder titleBuilder = new StringBuilder(" (");
if (config.showExact())
{
titleBuilder.append(QuantityFormatter.formatNumber(total));
}
else
{
titleBuilder.append(QuantityFormatter.quantityToStackSize(total));
}
titleBuilder.append(')');
// Append to title
String[] stringStack = client.getStringStack();
int stringStackSize = client.getStringStackSize();
stringStack[stringStackSize - 1] += titleBuilder.toString();
}
private void setLimitResetTime(int itemId)
{
Instant lastDateTime = configManager.getRSProfileConfiguration(GrandExchangeConfig.CONFIG_GROUP,
BUY_LIMIT_KEY + "." + itemId, Instant.class);
if (lastDateTime == null || lastDateTime.isBefore(Instant.now()))
{
configManager.setRSProfileConfiguration(GrandExchangeConfig.CONFIG_GROUP, BUY_LIMIT_KEY + "." + itemId,
Instant.now().plus(BUY_LIMIT_RESET));
}
}
private Instant getLimitResetTime(int itemId)
{
Instant lastDateTime = configManager.getRSProfileConfiguration(GrandExchangeConfig.CONFIG_GROUP,
BUY_LIMIT_KEY + "." + itemId, Instant.class);
if (lastDateTime == null)
{
return null;
}
if (lastDateTime.isBefore(Instant.now()))
{
configManager.unsetRSProfileConfiguration(GrandExchangeConfig.CONFIG_GROUP, BUY_LIMIT_KEY + "." + itemId);
return null;
}
return lastDateTime;
}
private void updateLimitTimer(GrandExchangeOffer offer)
{
if (offer.getState() == GrandExchangeOfferState.BOUGHT ||
(offer.getQuantitySold() > 0 &&
offer.getState() == GrandExchangeOfferState.BUYING))
{
setLimitResetTime(offer.getItemId());
}
}
private void rebuildGeText()
{
if (grandExchangeText == null || grandExchangeItem == null || grandExchangeItem.isHidden())
{
return;
}
final Widget geText = grandExchangeText;
final int itemId = grandExchangeItem.getItemId();
if (itemId == OFFER_DEFAULT_ITEM_ID || itemId == -1)
{
// This item is invalid/nothing has been searched for
return;
}
if (geText.getText() == grandExchangeExamine)
{
// if we've already set the text, don't set it again
return;
}
String text = geText.getText();
if (config.enableGELimits())
{
final ItemStats itemStats = itemManager.getItemStats(itemId, false);
// If we have item buy limit, append it
if (itemStats != null && itemStats.getGeLimit() > 0)
{
text += BUY_LIMIT_GE_TEXT + QuantityFormatter.formatNumber(itemStats.getGeLimit());
}
}
if (config.enableGELimitReset())
{
Instant resetTime = getLimitResetTime(itemId);
if (resetTime != null)
{
Duration remaining = Duration.between(Instant.now(), resetTime);
text += " (" + DurationFormatUtils.formatDuration(remaining.toMillis(), "H:mm") + ")";
}
}
grandExchangeExamine = text;
geText.setText(text);
if (!config.enableOsbPrices())
{
return;
}
// If we already have the result, use it
if (osbGrandExchangeResult != null && osbGrandExchangeResult.getItem_id() == itemId && osbGrandExchangeResult.getOverall_average() > 0)
{
grandExchangeExamine = text + OSB_GE_TEXT + QuantityFormatter.formatNumber(osbGrandExchangeResult.getOverall_average());
geText.setText(grandExchangeExamine);
}
if (osbItem == itemId)
{
// avoid starting duplicate lookups
return;
}
osbItem = itemId;
log.debug("Looking up OSB item price {}", itemId);
final String start = text;
executorService.submit(() ->
{
try
{
final OSBGrandExchangeResult result = osbGrandExchangeClient.lookupItem(itemId);
if (result != null && result.getOverall_average() > 0)
{
osbGrandExchangeResult = result;
// Update the text on the widget too
grandExchangeExamine = start + OSB_GE_TEXT + QuantityFormatter.formatNumber(result.getOverall_average());
geText.setText(grandExchangeExamine);
}
}
catch (IOException e)
{
log.debug("Error getting price of item {}", itemId, e);
}
});
}
static void openGeLink(String name, int itemId)
{
final String url = "https://services.runescape.com/m=itemdb_oldschool/"
+ name.replaceAll(" ", "+")
+ "/viewitem?obj="
+ itemId;
LinkBrowser.browse(url);
}
private String getMachineUuid()
{
String username = client.getUsername();
if (lastUsername == username)
{
return machineUuid;
}
lastUsername = username;
try
{
Hasher hasher = Hashing.sha256().newHasher();
Runtime runtime = Runtime.getRuntime();
hasher.putByte((byte) OSType.getOSType().ordinal());
hasher.putByte((byte) runtime.availableProcessors());
hasher.putUnencodedChars(System.getProperty("os.arch", ""));
hasher.putUnencodedChars(System.getProperty("os.version", ""));
hasher.putUnencodedChars(System.getProperty("user.name", ""));
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements())
{
NetworkInterface networkInterface = networkInterfaces.nextElement();
byte[] hardwareAddress = networkInterface.getHardwareAddress();
if (hardwareAddress != null)
{
hasher.putBytes(hardwareAddress);
}
}
hasher.putUnencodedChars(username);
machineUuid = hasher.hash().toString();
return machineUuid;
}
catch (SocketException ex)
{
log.debug("unable to generate machine id", ex);
machineUuid = null;
return null;
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2020, Dennis <me@dennis.dev>
* 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.grandexchange;
public enum GrandExchangeSearchMode
{
DEFAULT,
FUZZY_FALLBACK,
FUZZY_ONLY
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* Copyright (c) 2018, Psikoi <https://github.com/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.grandexchange;
import com.google.common.base.Strings;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import net.runelite.api.ItemComposition;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.game.ItemManager;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.components.IconTextField;
import net.runelite.client.ui.components.PluginErrorPanel;
import net.runelite.client.util.AsyncBufferedImage;
import net.runelite.http.api.item.ItemPrice;
import net.runelite.http.api.item.ItemStats;
/**
* This panel holds the search section of the Grand Exchange Plugin.
* It should display a search bar and either item results or a error panel.
*/
class GrandExchangeSearchPanel extends JPanel
{
private static final String ERROR_PANEL = "ERROR_PANEL";
private static final String RESULTS_PANEL = "RESULTS_PANEL";
private static final int MAX_SEARCH_ITEMS = 100;
private final GridBagConstraints constraints = new GridBagConstraints();
private final CardLayout cardLayout = new CardLayout();
private final ClientThread clientThread;
private final ItemManager itemManager;
private final ScheduledExecutorService executor;
private final IconTextField searchBar = new IconTextField();
/* The results container, this will hold all the individual ge item panels */
private final JPanel searchItemsPanel = new JPanel();
/* The center panel, this holds either the error panel or the results container */
private final JPanel centerPanel = new JPanel(cardLayout);
/* The error panel, this displays an error message */
private final PluginErrorPanel errorPanel = new PluginErrorPanel();
private final List<GrandExchangeItems> itemsList = new ArrayList<>();
GrandExchangeSearchPanel(ClientThread clientThread, ItemManager itemManager, ScheduledExecutorService executor)
{
this.clientThread = clientThread;
this.itemManager = itemManager;
this.executor = executor;
setLayout(new BorderLayout());
setBackground(ColorScheme.DARK_GRAY_COLOR);
/* The main container, this holds the search bar and the center panel */
JPanel container = new JPanel();
container.setLayout(new BorderLayout(5, 5));
container.setBorder(new EmptyBorder(10, 10, 10, 10));
container.setBackground(ColorScheme.DARK_GRAY_COLOR);
searchBar.setIcon(IconTextField.Icon.SEARCH);
searchBar.setPreferredSize(new Dimension(100, 30));
searchBar.setBackground(ColorScheme.DARKER_GRAY_COLOR);
searchBar.setHoverBackgroundColor(ColorScheme.DARK_GRAY_HOVER_COLOR);
searchBar.addActionListener(e -> executor.execute(() -> priceLookup(false)));
searchBar.addClearListener(this::updateSearch);
searchItemsPanel.setLayout(new GridBagLayout());
searchItemsPanel.setBackground(ColorScheme.DARK_GRAY_COLOR);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1;
constraints.gridx = 0;
constraints.gridy = 0;
/* This panel wraps the results panel and guarantees the scrolling behaviour */
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setBackground(ColorScheme.DARK_GRAY_COLOR);
wrapper.add(searchItemsPanel, BorderLayout.NORTH);
/* The results wrapper, this scrolling panel wraps the results container */
JScrollPane resultsWrapper = new JScrollPane(wrapper);
resultsWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR);
resultsWrapper.getVerticalScrollBar().setPreferredSize(new Dimension(12, 0));
resultsWrapper.getVerticalScrollBar().setBorder(new EmptyBorder(0, 5, 0, 0));
resultsWrapper.setVisible(false);
/* This panel wraps the error panel and limits its height */
JPanel errorWrapper = new JPanel(new BorderLayout());
errorWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR);
errorWrapper.add(errorPanel, BorderLayout.NORTH);
errorPanel.setContent("Grand Exchange Search",
"Here you can search for an item by its name to find price information.");
centerPanel.add(resultsWrapper, RESULTS_PANEL);
centerPanel.add(errorWrapper, ERROR_PANEL);
cardLayout.show(centerPanel, ERROR_PANEL);
container.add(searchBar, BorderLayout.NORTH);
container.add(centerPanel, BorderLayout.CENTER);
add(container, BorderLayout.CENTER);
}
void priceLookup(String item)
{
searchBar.setText(item);
executor.execute(() -> priceLookup(true));
}
private boolean updateSearch()
{
String lookup = searchBar.getText();
if (Strings.isNullOrEmpty(lookup))
{
searchItemsPanel.removeAll();
SwingUtilities.invokeLater(searchItemsPanel::updateUI);
return false;
}
// Input is not empty, add searching label
searchItemsPanel.removeAll();
searchBar.setBackground(ColorScheme.DARKER_GRAY_COLOR);
searchBar.setEditable(false);
searchBar.setIcon(IconTextField.Icon.LOADING);
return true;
}
private void priceLookup(boolean exactMatch)
{
if (!updateSearch())
{
return;
}
List<ItemPrice> result = itemManager.search(searchBar.getText());
if (result.isEmpty())
{
searchBar.setIcon(IconTextField.Icon.ERROR);
errorPanel.setContent("No results found.", "No items were found with that name, please try again.");
cardLayout.show(centerPanel, ERROR_PANEL);
searchBar.setEditable(true);
return;
}
// move to client thread to lookup item composition
clientThread.invokeLater(() -> processResult(result, searchBar.getText(), exactMatch));
}
private void processResult(List<ItemPrice> result, String lookup, boolean exactMatch)
{
itemsList.clear();
cardLayout.show(centerPanel, RESULTS_PANEL);
int count = 0;
for (ItemPrice item : result)
{
if (count++ > MAX_SEARCH_ITEMS)
{
// Cap search
break;
}
int itemId = item.getId();
ItemComposition itemComp = itemManager.getItemComposition(itemId);
ItemStats itemStats = itemManager.getItemStats(itemId, false);
int itemPrice = item.getPrice();
int itemLimit = itemStats != null ? itemStats.getGeLimit() : 0;
AsyncBufferedImage itemImage = itemManager.getImage(itemId);
itemsList.add(new GrandExchangeItems(itemImage, item.getName(), itemId, itemPrice, itemComp.getPrice() * 0.6, itemLimit));
// If using hotkey to lookup item, stop after finding match.
if (exactMatch && item.getName().equalsIgnoreCase(lookup))
{
break;
}
}
SwingUtilities.invokeLater(() ->
{
int index = 0;
for (GrandExchangeItems item : itemsList)
{
GrandExchangeItemPanel panel = new GrandExchangeItemPanel(item.getIcon(), item.getName(),
item.getItemId(), item.getGePrice(), item.getHaPrice(), item.getGeItemLimit());
/*
Add the first item directly, wrap the rest with margin. This margin hack is because
gridbaglayout does not support inter-element margins.
*/
if (index++ > 0)
{
JPanel marginWrapper = new JPanel(new BorderLayout());
marginWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR);
marginWrapper.setBorder(new EmptyBorder(5, 0, 0, 0));
marginWrapper.add(panel, BorderLayout.NORTH);
searchItemsPanel.add(marginWrapper, constraints);
}
else
{
searchItemsPanel.add(panel, constraints);
}
constraints.gridy++;
}
// if exactMatch was set, then it came from the applet, so don't lose focus
if (!exactMatch)
{
searchItemsPanel.requestFocusInWindow();
}
searchBar.setEditable(true);
// Remove searching label after search is complete
if (!itemsList.isEmpty())
{
searchBar.setIcon(IconTextField.Icon.SEARCH);
}
});
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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.grandexchange;
import lombok.Data;
import net.runelite.api.GrandExchangeOfferState;
@Data
class SavedOffer
{
private int itemId;
private int quantitySold;
private int totalQuantity;
private int price;
private int spent;
private GrandExchangeOfferState state;
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.grounditems;
import java.time.Instant;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.Builder;
import lombok.Data;
import lombok.Value;
import net.runelite.api.coords.WorldPoint;
@Data
@Builder
class GroundItem
{
private int id;
private int itemId;
private String name;
private int quantity;
private WorldPoint location;
private int height;
private int haPrice;
private int gePrice;
private int offset;
private boolean tradeable;
@Nonnull
private LootType lootType;
@Nullable
private Instant spawnTime;
private boolean stackable;
int getHaPrice()
{
return haPrice * quantity;
}
int getGePrice()
{
return gePrice * quantity;
}
boolean isMine()
{
return lootType != LootType.UNKNOWN;
}
@Value
static class GroundItemKey
{
private int itemId;
private WorldPoint location;
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* 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.grounditems;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.time.Duration;
import java.time.Instant;
import javax.inject.Inject;
import javax.swing.SwingUtilities;
import net.runelite.client.input.KeyListener;
import net.runelite.client.input.MouseAdapter;
public class GroundItemInputListener extends MouseAdapter implements KeyListener
{
private static final int HOTKEY = KeyEvent.VK_ALT;
private Instant lastPress;
@Inject
private GroundItemsPlugin plugin;
@Inject
private GroundItemsConfig config;
@Override
public void keyTyped(KeyEvent e)
{
}
@Override
public void keyPressed(KeyEvent e)
{
if (e.getKeyCode() == HOTKEY)
{
if (plugin.isHideAll())
{
plugin.setHideAll(false);
plugin.setHotKeyPressed(true);
lastPress = null;
}
else if (lastPress != null && !plugin.isHotKeyPressed() && config.doubleTapDelay() > 0 && Duration.between(lastPress, Instant.now()).compareTo(Duration.ofMillis(config.doubleTapDelay())) < 0)
{
plugin.setHideAll(true);
lastPress = null;
}
else
{
plugin.setHotKeyPressed(true);
lastPress = Instant.now();
}
}
}
@Override
public void keyReleased(KeyEvent e)
{
if (e.getKeyCode() == HOTKEY)
{
plugin.setHotKeyPressed(false);
plugin.setTextBoxBounds(null);
plugin.setHiddenBoxBounds(null);
plugin.setHighlightBoxBounds(null);
}
}
@Override
public MouseEvent mousePressed(MouseEvent e)
{
final Point mousePos = e.getPoint();
if (plugin.isHotKeyPressed())
{
if (SwingUtilities.isLeftMouseButton(e))
{
// Process both click boxes for hidden and highlighted items
if (plugin.getHiddenBoxBounds() != null && plugin.getHiddenBoxBounds().getKey().contains(mousePos))
{
plugin.updateList(plugin.getHiddenBoxBounds().getValue().getName(), true);
e.consume();
return e;
}
if (plugin.getHighlightBoxBounds() != null && plugin.getHighlightBoxBounds().getKey().contains(mousePos))
{
plugin.updateList(plugin.getHighlightBoxBounds().getValue().getName(), false);
e.consume();
return e;
}
// There is one name click box for left click and one for right click
if (plugin.getTextBoxBounds() != null && plugin.getTextBoxBounds().getKey().contains(mousePos))
{
plugin.updateList(plugin.getTextBoxBounds().getValue().getName(), false);
e.consume();
return e;
}
}
else if (SwingUtilities.isRightMouseButton(e))
{
if (plugin.getTextBoxBounds() != null && plugin.getTextBoxBounds().getKey().contains(mousePos))
{
plugin.updateList(plugin.getTextBoxBounds().getValue().getName(), true);
e.consume();
return e;
}
}
}
return e;
}
}

View File

@@ -0,0 +1,398 @@
/*
* Copyright (c) 2017, Aria <aria@ar1as.space>
* 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.grounditems;
import java.awt.Color;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Units;
import net.runelite.client.config.ConfigSection;
import net.runelite.client.plugins.grounditems.config.DespawnTimerMode;
import net.runelite.client.plugins.grounditems.config.HighlightTier;
import net.runelite.client.plugins.grounditems.config.ItemHighlightMode;
import net.runelite.client.plugins.grounditems.config.MenuHighlightMode;
import net.runelite.client.plugins.grounditems.config.PriceDisplayMode;
import net.runelite.client.plugins.grounditems.config.ValueCalculationMode;
@ConfigGroup("grounditems")
public interface GroundItemsConfig extends Config
{
@ConfigSection(
name = "Item Lists",
description = "The highlighted and hidden item lists",
position = 0,
closedByDefault = true
)
String itemLists = "itemLists";
@ConfigItem(
keyName = "highlightedItems",
name = "Highlighted Items",
description = "Configures specifically highlighted ground items. Format: (item), (item)",
position = 0,
section = itemLists
)
default String getHighlightItems()
{
return "";
}
@ConfigItem(
keyName = "highlightedItems",
name = "",
description = ""
)
void setHighlightedItem(String key);
@ConfigItem(
keyName = "hiddenItems",
name = "Hidden Items",
description = "Configures hidden ground items. Format: (item), (item)",
position = 1,
section = itemLists
)
default String getHiddenItems()
{
return "Vial, Ashes, Coins, Bones, Bucket, Jug, Seaweed";
}
@ConfigItem(
keyName = "hiddenItems",
name = "",
description = ""
)
void setHiddenItems(String key);
@ConfigItem(
keyName = "showHighlightedOnly",
name = "Show Highlighted items only",
description = "Configures whether or not to draw items only on your highlighted list",
position = 2
)
default boolean showHighlightedOnly()
{
return false;
}
@ConfigItem(
keyName = "dontHideUntradeables",
name = "Do not hide untradeables",
description = "Configures whether or not untradeable items ignore hiding under settings",
position = 3
)
default boolean dontHideUntradeables()
{
return true;
}
@ConfigItem(
keyName = "showMenuItemQuantities",
name = "Show Menu Item Quantities",
description = "Configures whether or not to show the item quantities in the menu",
position = 4
)
default boolean showMenuItemQuantities()
{
return true;
}
@ConfigItem(
keyName = "recolorMenuHiddenItems",
name = "Recolor Menu Hidden Items",
description = "Configures whether or not hidden items in right-click menu will be recolored",
position = 5
)
default boolean recolorMenuHiddenItems()
{
return false;
}
@ConfigItem(
keyName = "highlightTiles",
name = "Highlight Tiles",
description = "Configures whether or not to highlight tiles containing ground items",
position = 6
)
default boolean highlightTiles()
{
return false;
}
@ConfigItem(
keyName = "notifyHighlightedDrops",
name = "Notify for Highlighted drops",
description = "Configures whether or not to notify for drops on your highlighted list",
position = 7
)
default boolean notifyHighlightedDrops()
{
return false;
}
@ConfigItem(
keyName = "notifyTier",
name = "Notify >= Tier",
description = "Configures which price tiers will trigger a notification on drop",
position = 8
)
default HighlightTier notifyTier()
{
return HighlightTier.OFF;
}
@ConfigItem(
keyName = "priceDisplayMode",
name = "Price Display Mode",
description = "Configures which price types are shown alongside ground item name",
position = 9
)
default PriceDisplayMode priceDisplayMode()
{
return PriceDisplayMode.BOTH;
}
@ConfigItem(
keyName = "itemHighlightMode",
name = "Item Highlight Mode",
description = "Configures how ground items will be highlighted",
position = 10
)
default ItemHighlightMode itemHighlightMode()
{
return ItemHighlightMode.BOTH;
}
@ConfigItem(
keyName = "menuHighlightMode",
name = "Menu Highlight Mode",
description = "Configures what to highlight in right-click menu",
position = 11
)
default MenuHighlightMode menuHighlightMode()
{
return MenuHighlightMode.NAME;
}
@ConfigItem(
keyName = "highlightValueCalculation",
name = "Highlight Value Calculation",
description = "Configures which coin value is used to determine highlight color",
position = 12
)
default ValueCalculationMode valueCalculationMode()
{
return ValueCalculationMode.HIGHEST;
}
@ConfigItem(
keyName = "hideUnderValue",
name = "Hide < Value",
description = "Configures hidden ground items under both GE and HA value",
position = 13
)
default int getHideUnderValue()
{
return 0;
}
@ConfigItem(
keyName = "defaultColor",
name = "Default items color",
description = "Configures the color for default, non-highlighted items",
position = 14
)
default Color defaultColor()
{
return Color.WHITE;
}
@ConfigItem(
keyName = "highlightedColor",
name = "Highlighted items color",
description = "Configures the color for highlighted items",
position = 15
)
default Color highlightedColor()
{
return Color.decode("#AA00FF");
}
@ConfigItem(
keyName = "hiddenColor",
name = "Hidden items color",
description = "Configures the color for hidden items in right-click menu and when holding ALT",
position = 16
)
default Color hiddenColor()
{
return Color.GRAY;
}
@ConfigItem(
keyName = "lowValueColor",
name = "Low value items color",
description = "Configures the color for low value items",
position = 17
)
default Color lowValueColor()
{
return Color.decode("#66B2FF");
}
@ConfigItem(
keyName = "lowValuePrice",
name = "Low value price",
description = "Configures the start price for low value items",
position = 18
)
default int lowValuePrice()
{
return 20000;
}
@ConfigItem(
keyName = "mediumValueColor",
name = "Medium value items color",
description = "Configures the color for medium value items",
position = 19
)
default Color mediumValueColor()
{
return Color.decode("#99FF99");
}
@ConfigItem(
keyName = "mediumValuePrice",
name = "Medium value price",
description = "Configures the start price for medium value items",
position = 20
)
default int mediumValuePrice()
{
return 100000;
}
@ConfigItem(
keyName = "highValueColor",
name = "High value items color",
description = "Configures the color for high value items",
position = 21
)
default Color highValueColor()
{
return Color.decode("#FF9600");
}
@ConfigItem(
keyName = "highValuePrice",
name = "High value price",
description = "Configures the start price for high value items",
position = 22
)
default int highValuePrice()
{
return 1000000;
}
@ConfigItem(
keyName = "insaneValueColor",
name = "Insane value items color",
description = "Configures the color for insane value items",
position = 23
)
default Color insaneValueColor()
{
return Color.decode("#FF66B2");
}
@ConfigItem(
keyName = "insaneValuePrice",
name = "Insane value price",
description = "Configures the start price for insane value items",
position = 24
)
default int insaneValuePrice()
{
return 10000000;
}
@ConfigItem(
keyName = "onlyShowLoot",
name = "Only show loot",
description = "Only shows drops from NPCs and players",
position = 25
)
default boolean onlyShowLoot()
{
return false;
}
@ConfigItem(
keyName = "doubleTapDelay",
name = "Delay for double-tap ALT to hide",
description = "Decrease this number if you accidentally hide ground items often. (0 = Disabled)",
position = 26
)
@Units(Units.MILLISECONDS)
default int doubleTapDelay()
{
return 250;
}
@ConfigItem(
keyName = "collapseEntries",
name = "Collapse ground item menu entries",
description = "Collapses ground item menu entries together and appends count",
position = 27
)
default boolean collapseEntries()
{
return false;
}
@ConfigItem(
keyName = "groundItemTimers",
name = "Despawn timer",
description = "Shows despawn timers for items you've dropped and received as loot",
position = 28
)
default DespawnTimerMode groundItemTimers()
{
return DespawnTimerMode.OFF;
}
@ConfigItem(
keyName = "textOutline",
name = "Text Outline",
description = "Use an outline around text instead of a text shadow",
position = 29
)
default boolean textOutline()
{
return false;
}
}

View File

@@ -0,0 +1,574 @@
/*
* Copyright (c) 2017, Aria <aria@ar1as.space>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.grounditems;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.Perspective;
import net.runelite.api.Player;
import net.runelite.api.Point;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import static net.runelite.client.plugins.grounditems.GroundItemsPlugin.MAX_QUANTITY;
import net.runelite.client.plugins.grounditems.config.DespawnTimerMode;
import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.MENU;
import net.runelite.client.plugins.grounditems.config.PriceDisplayMode;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayUtil;
import net.runelite.client.ui.overlay.components.BackgroundComponent;
import net.runelite.client.ui.overlay.components.ProgressPieComponent;
import net.runelite.client.ui.overlay.components.TextComponent;
import net.runelite.client.util.QuantityFormatter;
import org.apache.commons.lang3.ArrayUtils;
public class GroundItemsOverlay extends Overlay
{
private static final int MAX_DISTANCE = 2500;
// We must offset the text on the z-axis such that
// it doesn't obscure the ground items below it.
private static final int OFFSET_Z = 20;
// The 15 pixel gap between each drawn ground item.
private static final int STRING_GAP = 15;
// Size of the hidden/highlight boxes
private static final int RECTANGLE_SIZE = 8;
private static final Color PUBLIC_TIMER_COLOR = Color.YELLOW;
private static final Color PRIVATE_TIMER_COLOR = Color.GREEN;
private static final int TIMER_OVERLAY_DIAMETER = 10;
private static final Duration DESPAWN_TIME_INSTANCE = Duration.ofMinutes(30);
private static final Duration DESPAWN_TIME_LOOT = Duration.ofMinutes(2);
private static final Duration DESPAWN_TIME_DROP = Duration.ofMinutes(3);
private static final int KRAKEN_REGION = 9116;
private static final int KBD_NMZ_REGION = 9033;
private final Client client;
private final GroundItemsPlugin plugin;
private final GroundItemsConfig config;
private final StringBuilder itemStringBuilder = new StringBuilder();
private final BackgroundComponent backgroundComponent = new BackgroundComponent();
private final TextComponent textComponent = new TextComponent();
private final ProgressPieComponent progressPieComponent = new ProgressPieComponent();
private final Map<WorldPoint, Integer> offsetMap = new HashMap<>();
@Inject
private GroundItemsOverlay(Client client, GroundItemsPlugin plugin, GroundItemsConfig config)
{
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_SCENE);
this.client = client;
this.plugin = plugin;
this.config = config;
}
@Override
public Dimension render(Graphics2D graphics)
{
final boolean dontShowOverlay = (config.itemHighlightMode() == MENU || plugin.isHideAll()) && !plugin.isHotKeyPressed();
if (dontShowOverlay && !config.highlightTiles())
{
return null;
}
final FontMetrics fm = graphics.getFontMetrics();
final Player player = client.getLocalPlayer();
if (player == null || client.getViewportWidget() == null)
{
return null;
}
offsetMap.clear();
final LocalPoint localLocation = player.getLocalLocation();
final Point mousePos = client.getMouseCanvasPosition();
Collection<GroundItem> groundItemList = plugin.getCollectedGroundItems().values();
GroundItem topGroundItem = null;
if (plugin.isHotKeyPressed())
{
// Make copy of ground items because we are going to modify them here, and the array list supports our
// desired behaviour here
groundItemList = new ArrayList<>(groundItemList);
final java.awt.Point awtMousePos = new java.awt.Point(mousePos.getX(), mousePos.getY());
GroundItem groundItem = null;
for (GroundItem item : groundItemList)
{
item.setOffset(offsetMap.compute(item.getLocation(), (k, v) -> v != null ? v + 1 : 0));
if (groundItem != null)
{
continue;
}
if (plugin.getTextBoxBounds() != null
&& item.equals(plugin.getTextBoxBounds().getValue())
&& plugin.getTextBoxBounds().getKey().contains(awtMousePos))
{
groundItem = item;
continue;
}
if (plugin.getHiddenBoxBounds() != null
&& item.equals(plugin.getHiddenBoxBounds().getValue())
&& plugin.getHiddenBoxBounds().getKey().contains(awtMousePos))
{
groundItem = item;
continue;
}
if (plugin.getHighlightBoxBounds() != null
&& item.equals(plugin.getHighlightBoxBounds().getValue())
&& plugin.getHighlightBoxBounds().getKey().contains(awtMousePos))
{
groundItem = item;
}
}
if (groundItem != null)
{
groundItemList.remove(groundItem);
groundItemList.add(groundItem);
topGroundItem = groundItem;
}
}
plugin.setTextBoxBounds(null);
plugin.setHiddenBoxBounds(null);
plugin.setHighlightBoxBounds(null);
final boolean onlyShowLoot = config.onlyShowLoot();
final DespawnTimerMode groundItemTimers = config.groundItemTimers();
final boolean outline = config.textOutline();
for (GroundItem item : groundItemList)
{
final LocalPoint groundPoint = LocalPoint.fromWorld(client, item.getLocation());
if (groundPoint == null || localLocation.distanceTo(groundPoint) > MAX_DISTANCE
|| (onlyShowLoot && !item.isMine()))
{
continue;
}
final Color highlighted = plugin.getHighlighted(new NamedQuantity(item), item.getGePrice(), item.getHaPrice());
final Color hidden = plugin.getHidden(new NamedQuantity(item), item.getGePrice(), item.getHaPrice(), item.isTradeable());
if (highlighted == null && !plugin.isHotKeyPressed())
{
// Do not display hidden items
if (hidden != null)
{
continue;
}
// Do not display non-highlighted items
if (config.showHighlightedOnly())
{
continue;
}
}
final Color color = plugin.getItemColor(highlighted, hidden);
if (config.highlightTiles())
{
final Polygon poly = Perspective.getCanvasTilePoly(client, groundPoint, item.getHeight());
if (poly != null)
{
OverlayUtil.renderPolygon(graphics, poly, color);
}
}
if (dontShowOverlay)
{
continue;
}
itemStringBuilder.append(item.getName());
if (item.getQuantity() > 1)
{
if (item.getQuantity() >= MAX_QUANTITY)
{
itemStringBuilder.append(" (Lots!)");
}
else
{
itemStringBuilder.append(" (")
.append(QuantityFormatter.quantityToStackSize(item.getQuantity()))
.append(")");
}
}
if (config.priceDisplayMode() == PriceDisplayMode.BOTH)
{
if (item.getGePrice() > 0)
{
itemStringBuilder.append(" (GE: ")
.append(QuantityFormatter.quantityToStackSize(item.getGePrice()))
.append(" gp)");
}
if (item.getHaPrice() > 0)
{
itemStringBuilder.append(" (HA: ")
.append(QuantityFormatter.quantityToStackSize(item.getHaPrice()))
.append(" gp)");
}
}
else if (config.priceDisplayMode() != PriceDisplayMode.OFF)
{
final int price = config.priceDisplayMode() == PriceDisplayMode.GE
? item.getGePrice()
: item.getHaPrice();
if (price > 0)
{
itemStringBuilder
.append(" (")
.append(QuantityFormatter.quantityToStackSize(price))
.append(" gp)");
}
}
final String itemString = itemStringBuilder.toString();
itemStringBuilder.setLength(0);
final Point textPoint = Perspective.getCanvasTextLocation(client,
graphics,
groundPoint,
itemString,
item.getHeight() + OFFSET_Z);
if (textPoint == null)
{
continue;
}
final int offset = plugin.isHotKeyPressed()
? item.getOffset()
: offsetMap.compute(item.getLocation(), (k, v) -> v != null ? v + 1 : 0);
final int textX = textPoint.getX();
final int textY = textPoint.getY() - (STRING_GAP * offset);
if (plugin.isHotKeyPressed())
{
final int stringWidth = fm.stringWidth(itemString);
final int stringHeight = fm.getHeight();
// Item bounds
int x = textX - 2;
int y = textY - stringHeight - 2;
int width = stringWidth + 4;
int height = stringHeight + 4;
final Rectangle itemBounds = new Rectangle(x, y, width, height);
// Hidden box
x += width + 2;
y = textY - (RECTANGLE_SIZE + stringHeight) / 2;
width = height = RECTANGLE_SIZE;
final Rectangle itemHiddenBox = new Rectangle(x, y, width, height);
// Highlight box
x += width + 2;
final Rectangle itemHighlightBox = new Rectangle(x, y, width, height);
boolean mouseInBox = itemBounds.contains(mousePos.getX(), mousePos.getY());
boolean mouseInHiddenBox = itemHiddenBox.contains(mousePos.getX(), mousePos.getY());
boolean mouseInHighlightBox = itemHighlightBox.contains(mousePos.getX(), mousePos.getY());
if (mouseInBox)
{
plugin.setTextBoxBounds(new SimpleEntry<>(itemBounds, item));
}
else if (mouseInHiddenBox)
{
plugin.setHiddenBoxBounds(new SimpleEntry<>(itemHiddenBox, item));
}
else if (mouseInHighlightBox)
{
plugin.setHighlightBoxBounds(new SimpleEntry<>(itemHighlightBox, item));
}
boolean topItem = topGroundItem == item;
// Draw background if hovering
if (topItem && (mouseInBox || mouseInHiddenBox || mouseInHighlightBox))
{
backgroundComponent.setRectangle(itemBounds);
backgroundComponent.render(graphics);
}
// Draw hidden box
drawRectangle(graphics, itemHiddenBox, topItem && mouseInHiddenBox ? Color.RED : color, hidden != null, true);
// Draw highlight box
drawRectangle(graphics, itemHighlightBox, topItem && mouseInHighlightBox ? Color.GREEN : color, highlighted != null, false);
}
// When the hotkey is pressed the hidden/highlight boxes are drawn to the right of the text,
// so always draw the pie since it is on the left hand side.
if (groundItemTimers == DespawnTimerMode.PIE || plugin.isHotKeyPressed())
{
drawTimerPieOverlay(graphics, textX, textY, item);
}
else if (groundItemTimers == DespawnTimerMode.SECONDS || groundItemTimers == DespawnTimerMode.TICKS)
{
Instant despawnTime = calculateDespawnTime(item);
Color timerColor = getItemTimerColor(item);
if (despawnTime != null && timerColor != null)
{
long despawnTimeMillis = despawnTime.toEpochMilli() - Instant.now().toEpochMilli();
final String timerText;
if (groundItemTimers == DespawnTimerMode.SECONDS)
{
timerText = String.format(" - %.1f", despawnTimeMillis / 1000f);
}
else // TICKS
{
timerText = String.format(" - %d", despawnTimeMillis / 600);
}
// The timer text is drawn separately to have its own color, and is intentionally not included
// in the getCanvasTextLocation() call because the timer text can change per frame and we do not
// use a monospaced font, which causes the text location on screen to jump around slightly each frame.
textComponent.setText(timerText);
textComponent.setColor(timerColor);
textComponent.setOutline(outline);
textComponent.setPosition(new java.awt.Point(textX + fm.stringWidth(itemString), textY));
textComponent.render(graphics);
}
}
textComponent.setText(itemString);
textComponent.setColor(color);
textComponent.setOutline(outline);
textComponent.setPosition(new java.awt.Point(textX, textY));
textComponent.render(graphics);
}
return null;
}
private Instant calculateDespawnTime(GroundItem groundItem)
{
// We can only accurately guess despawn times for our own pvm loot and dropped items
if (groundItem.getLootType() != LootType.PVM && groundItem.getLootType() != LootType.DROPPED)
{
return null;
}
// Loot appears to others after 1 minute, and despawns after 2 minutes
// Dropped items appear to others after 1 minute, and despawns after 3 minutes
// Items in instances never appear to anyone and despawn after 30 minutes
Instant spawnTime = groundItem.getSpawnTime();
if (spawnTime == null)
{
return null;
}
Instant despawnTime;
Instant now = Instant.now();
if (client.isInInstancedRegion())
{
// Items in the Kraken instance appear to never despawn?
if (isInKraken())
{
return null;
}
else if (isInKBDorNMZ())
{
// NMZ and the KBD lair uses the same region ID but NMZ uses planes 1-3 and KBD uses plane 0
if (client.getLocalPlayer().getWorldLocation().getPlane() == 0)
{
// Items in the KBD instance use the standard despawn timer
if (groundItem.getLootType() == LootType.DROPPED)
{
despawnTime = spawnTime.plus(DESPAWN_TIME_DROP);
}
else
{
despawnTime = spawnTime.plus(DESPAWN_TIME_LOOT);
}
}
else
{
// Dropped items in the NMZ instance appear to never despawn?
if (groundItem.getLootType() == LootType.DROPPED)
{
return null;
}
else
{
despawnTime = spawnTime.plus(DESPAWN_TIME_LOOT);
}
}
}
else
{
despawnTime = spawnTime.plus(DESPAWN_TIME_INSTANCE);
}
}
else
{
if (groundItem.getLootType() == LootType.DROPPED)
{
despawnTime = spawnTime.plus(DESPAWN_TIME_DROP);
}
else
{
despawnTime = spawnTime.plus(DESPAWN_TIME_LOOT);
}
}
if (now.isBefore(spawnTime) || now.isAfter(despawnTime))
{
// that's weird
return null;
}
return despawnTime;
}
private Color getItemTimerColor(GroundItem groundItem)
{
// We can only accurately guess despawn times for our own pvm loot and dropped items
if (groundItem.getLootType() != LootType.PVM && groundItem.getLootType() != LootType.DROPPED)
{
return null;
}
final Instant spawnTime = groundItem.getSpawnTime();
if (spawnTime == null)
{
return null;
}
final Instant now = Instant.now();
// If it has not yet been a minute, the item is private
if (client.isInInstancedRegion() || spawnTime.plus(1, ChronoUnit.MINUTES).isAfter(now))
{
return PRIVATE_TIMER_COLOR;
}
else
{
return PUBLIC_TIMER_COLOR;
}
}
private void drawTimerPieOverlay(Graphics2D graphics, int textX, int textY, GroundItem groundItem)
{
Instant now = Instant.now();
Instant spawnTime = groundItem.getSpawnTime();
Instant despawnTime = calculateDespawnTime(groundItem);
Color fillColor = getItemTimerColor(groundItem);
if (spawnTime == null || despawnTime == null || fillColor == null)
{
return;
}
float percent = (float) (now.toEpochMilli() - spawnTime.toEpochMilli()) / (despawnTime.toEpochMilli() - spawnTime.toEpochMilli());
progressPieComponent.setDiameter(TIMER_OVERLAY_DIAMETER);
// Shift over to not be on top of the text
int x = textX - TIMER_OVERLAY_DIAMETER;
int y = textY - TIMER_OVERLAY_DIAMETER / 2;
progressPieComponent.setPosition(new Point(x, y));
progressPieComponent.setFill(fillColor);
progressPieComponent.setBorderColor(fillColor);
progressPieComponent.setProgress(1 - percent); // inverse so pie drains over time
progressPieComponent.render(graphics);
}
private void drawRectangle(Graphics2D graphics, Rectangle rect, Color color, boolean inList, boolean hiddenBox)
{
graphics.setColor(Color.BLACK);
graphics.drawRect(rect.x + 1, rect.y + 1, rect.width, rect.height);
graphics.setColor(color);
graphics.draw(rect);
if (inList)
{
graphics.fill(rect);
}
graphics.setColor(Color.WHITE);
// Minus symbol
graphics.drawLine
(
rect.x + 2,
rect.y + (rect.height / 2),
rect.x + rect.width - 2,
rect.y + (rect.height / 2)
);
if (!hiddenBox)
{
// Plus symbol
graphics.drawLine
(
rect.x + (rect.width / 2),
rect.y + 2,
rect.x + (rect.width / 2),
rect.y + rect.height - 2
);
}
}
private boolean isInKraken()
{
return ArrayUtils.contains(client.getMapRegions(), KRAKEN_REGION);
}
private boolean isInKBDorNMZ()
{
return ArrayUtils.contains(client.getMapRegions(), KBD_NMZ_REGION);
}
}

View File

@@ -0,0 +1,691 @@
/*
* Copyright (c) 2017, Aria <aria@ar1as.space>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* 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.grounditems;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.EvictingQueue;
import com.google.common.collect.ImmutableList;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.Rectangle;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.Value;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.ItemComposition;
import net.runelite.api.ItemID;
import net.runelite.api.MenuAction;
import net.runelite.api.MenuEntry;
import net.runelite.api.Player;
import net.runelite.api.Tile;
import net.runelite.api.TileItem;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ClientTick;
import net.runelite.api.events.FocusChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.ItemDespawned;
import net.runelite.api.events.ItemQuantityChanged;
import net.runelite.api.events.ItemSpawned;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.client.Notifier;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.NpcLootReceived;
import net.runelite.client.events.PlayerLootReceived;
import net.runelite.client.game.ItemManager;
import net.runelite.client.game.ItemStack;
import net.runelite.client.input.KeyManager;
import net.runelite.client.input.MouseManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.grounditems.config.HighlightTier;
import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.OVERLAY;
import net.runelite.client.plugins.grounditems.config.MenuHighlightMode;
import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.BOTH;
import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.NAME;
import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.OPTION;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.QuantityFormatter;
import net.runelite.client.util.Text;
@PluginDescriptor(
name = "Ground Items",
description = "Highlight ground items and/or show price information",
tags = {"grand", "exchange", "high", "alchemy", "prices", "highlight", "overlay"}
)
public class GroundItemsPlugin extends Plugin
{
@Value
static class PriceHighlight
{
private final int price;
private final Color color;
}
// The game won't send anything higher than this value to the plugin -
// so we replace any item quantity higher with "Lots" instead.
static final int MAX_QUANTITY = 65535;
// ItemID for coins
private static final int COINS = ItemID.COINS_995;
// Ground item menu options
private static final int FIRST_OPTION = MenuAction.GROUND_ITEM_FIRST_OPTION.getId();
private static final int SECOND_OPTION = MenuAction.GROUND_ITEM_SECOND_OPTION.getId();
private static final int THIRD_OPTION = MenuAction.GROUND_ITEM_THIRD_OPTION.getId(); // this is Take
private static final int FOURTH_OPTION = MenuAction.GROUND_ITEM_FOURTH_OPTION.getId();
private static final int FIFTH_OPTION = MenuAction.GROUND_ITEM_FIFTH_OPTION.getId();
private static final int EXAMINE_ITEM = MenuAction.EXAMINE_ITEM_GROUND.getId();
private static final int CAST_ON_ITEM = MenuAction.SPELL_CAST_ON_GROUND_ITEM.getId();
private static final String TELEGRAB_TEXT = ColorUtil.wrapWithColorTag("Telekinetic Grab", Color.GREEN) + ColorUtil.prependColorTag(" -> ", Color.WHITE);
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private Map.Entry<Rectangle, GroundItem> textBoxBounds;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private Map.Entry<Rectangle, GroundItem> hiddenBoxBounds;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private Map.Entry<Rectangle, GroundItem> highlightBoxBounds;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private boolean hotKeyPressed;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private boolean hideAll;
private List<String> hiddenItemList = new CopyOnWriteArrayList<>();
private List<String> highlightedItemsList = new CopyOnWriteArrayList<>();
@Inject
private GroundItemInputListener inputListener;
@Inject
private MouseManager mouseManager;
@Inject
private KeyManager keyManager;
@Inject
private Client client;
@Inject
private ItemManager itemManager;
@Inject
private OverlayManager overlayManager;
@Inject
private GroundItemsConfig config;
@Inject
private GroundItemsOverlay overlay;
@Inject
private Notifier notifier;
@Inject
private ScheduledExecutorService executor;
@Getter
private final Map<GroundItem.GroundItemKey, GroundItem> collectedGroundItems = new LinkedHashMap<>();
private List<PriceHighlight> priceChecks = ImmutableList.of();
private LoadingCache<NamedQuantity, Boolean> highlightedItems;
private LoadingCache<NamedQuantity, Boolean> hiddenItems;
private final Queue<Integer> droppedItemQueue = EvictingQueue.create(16); // recently dropped items
@Provides
GroundItemsConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(GroundItemsConfig.class);
}
@Override
protected void startUp()
{
overlayManager.add(overlay);
mouseManager.registerMouseListener(inputListener);
keyManager.registerKeyListener(inputListener);
executor.execute(this::reset);
}
@Override
protected void shutDown()
{
overlayManager.remove(overlay);
mouseManager.unregisterMouseListener(inputListener);
keyManager.unregisterKeyListener(inputListener);
highlightedItems.invalidateAll();
highlightedItems = null;
hiddenItems.invalidateAll();
hiddenItems = null;
hiddenItemList = null;
highlightedItemsList = null;
collectedGroundItems.clear();
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("grounditems"))
{
executor.execute(this::reset);
}
}
@Subscribe
public void onGameStateChanged(final GameStateChanged event)
{
if (event.getGameState() == GameState.LOADING)
{
collectedGroundItems.clear();
}
}
@Subscribe
public void onItemSpawned(ItemSpawned itemSpawned)
{
TileItem item = itemSpawned.getItem();
Tile tile = itemSpawned.getTile();
GroundItem groundItem = buildGroundItem(tile, item);
GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation());
GroundItem existing = collectedGroundItems.putIfAbsent(groundItemKey, groundItem);
if (existing != null)
{
existing.setQuantity(existing.getQuantity() + groundItem.getQuantity());
// The spawn time remains set at the oldest spawn
}
if (!config.onlyShowLoot())
{
notifyHighlightedItem(groundItem);
}
}
@Subscribe
public void onItemDespawned(ItemDespawned itemDespawned)
{
TileItem item = itemDespawned.getItem();
Tile tile = itemDespawned.getTile();
GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation());
GroundItem groundItem = collectedGroundItems.get(groundItemKey);
if (groundItem == null)
{
return;
}
if (groundItem.getQuantity() <= item.getQuantity())
{
collectedGroundItems.remove(groundItemKey);
}
else
{
groundItem.setQuantity(groundItem.getQuantity() - item.getQuantity());
// When picking up an item when multiple stacks appear on the ground,
// it is not known which item is picked up, so we invalidate the spawn
// time
groundItem.setSpawnTime(null);
}
}
@Subscribe
public void onItemQuantityChanged(ItemQuantityChanged itemQuantityChanged)
{
TileItem item = itemQuantityChanged.getItem();
Tile tile = itemQuantityChanged.getTile();
int oldQuantity = itemQuantityChanged.getOldQuantity();
int newQuantity = itemQuantityChanged.getNewQuantity();
int diff = newQuantity - oldQuantity;
GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation());
GroundItem groundItem = collectedGroundItems.get(groundItemKey);
if (groundItem != null)
{
groundItem.setQuantity(groundItem.getQuantity() + diff);
}
}
@Subscribe
public void onNpcLootReceived(NpcLootReceived npcLootReceived)
{
Collection<ItemStack> items = npcLootReceived.getItems();
lootReceived(items, LootType.PVM);
}
@Subscribe
public void onPlayerLootReceived(PlayerLootReceived playerLootReceived)
{
Collection<ItemStack> items = playerLootReceived.getItems();
lootReceived(items, LootType.PVP);
}
@Subscribe
public void onClientTick(ClientTick event)
{
if (!config.collapseEntries())
{
return;
}
final MenuEntry[] menuEntries = client.getMenuEntries();
final List<MenuEntryWithCount> newEntries = new ArrayList<>(menuEntries.length);
outer:
for (int i = menuEntries.length - 1; i >= 0; i--)
{
MenuEntry menuEntry = menuEntries[i];
int menuType = menuEntry.getType();
if (menuType == FIRST_OPTION || menuType == SECOND_OPTION || menuType == THIRD_OPTION
|| menuType == FOURTH_OPTION || menuType == FIFTH_OPTION || menuType == EXAMINE_ITEM)
{
for (MenuEntryWithCount entryWCount : newEntries)
{
if (entryWCount.getEntry().equals(menuEntry))
{
entryWCount.increment();
continue outer;
}
}
}
newEntries.add(new MenuEntryWithCount(menuEntry));
}
Collections.reverse(newEntries);
client.setMenuEntries(newEntries.stream().map(e ->
{
final MenuEntry entry = e.getEntry();
final int count = e.getCount();
if (count > 1)
{
entry.setTarget(entry.getTarget() + " x " + count);
}
return entry;
}).toArray(MenuEntry[]::new));
}
private void lootReceived(Collection<ItemStack> items, LootType lootType)
{
for (ItemStack itemStack : items)
{
WorldPoint location = WorldPoint.fromLocal(client, itemStack.getLocation());
GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(itemStack.getId(), location);
GroundItem groundItem = collectedGroundItems.get(groundItemKey);
if (groundItem != null)
{
groundItem.setLootType(lootType);
if (config.onlyShowLoot())
{
notifyHighlightedItem(groundItem);
}
}
}
}
private GroundItem buildGroundItem(final Tile tile, final TileItem item)
{
// Collect the data for the item
final int itemId = item.getId();
final ItemComposition itemComposition = itemManager.getItemComposition(itemId);
final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId;
final int alchPrice = itemComposition.getHaPrice();
final boolean dropped = tile.getWorldLocation().equals(client.getLocalPlayer().getWorldLocation()) && droppedItemQueue.remove(itemId);
final GroundItem groundItem = GroundItem.builder()
.id(itemId)
.location(tile.getWorldLocation())
.itemId(realItemId)
.quantity(item.getQuantity())
.name(itemComposition.getName())
.haPrice(alchPrice)
.height(tile.getItemLayer().getHeight())
.tradeable(itemComposition.isTradeable())
.lootType(dropped ? LootType.DROPPED : LootType.UNKNOWN)
.spawnTime(Instant.now())
.stackable(itemComposition.isStackable())
.build();
// Update item price in case it is coins
if (realItemId == COINS)
{
groundItem.setHaPrice(1);
groundItem.setGePrice(1);
}
else
{
groundItem.setGePrice(itemManager.getItemPrice(realItemId));
}
return groundItem;
}
private void reset()
{
// gets the hidden items from the text box in the config
hiddenItemList = Text.fromCSV(config.getHiddenItems());
// gets the highlighted items from the text box in the config
highlightedItemsList = Text.fromCSV(config.getHighlightItems());
highlightedItems = CacheBuilder.newBuilder()
.maximumSize(512L)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(new WildcardMatchLoader(highlightedItemsList));
hiddenItems = CacheBuilder.newBuilder()
.maximumSize(512L)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(new WildcardMatchLoader(hiddenItemList));
// Cache colors
ImmutableList.Builder<PriceHighlight> priceCheckBuilder = ImmutableList.builder();
if (config.insaneValuePrice() > 0)
{
priceCheckBuilder.add(new PriceHighlight(config.insaneValuePrice(), config.insaneValueColor()));
}
if (config.highValuePrice() > 0)
{
priceCheckBuilder.add(new PriceHighlight(config.highValuePrice(), config.highValueColor()));
}
if (config.mediumValuePrice() > 0)
{
priceCheckBuilder.add(new PriceHighlight(config.mediumValuePrice(), config.mediumValueColor()));
}
if (config.lowValuePrice() > 0)
{
priceCheckBuilder.add(new PriceHighlight(config.lowValuePrice(), config.lowValueColor()));
}
priceChecks = priceCheckBuilder.build();
}
@Subscribe
public void onMenuEntryAdded(MenuEntryAdded event)
{
if (config.itemHighlightMode() != OVERLAY)
{
final boolean telegrabEntry = event.getOption().equals("Cast") && event.getTarget().startsWith(TELEGRAB_TEXT) && event.getType() == CAST_ON_ITEM;
if (!(event.getOption().equals("Take") && event.getType() == THIRD_OPTION) && !telegrabEntry)
{
return;
}
final int itemId = event.getIdentifier();
final int sceneX = event.getActionParam0();
final int sceneY = event.getActionParam1();
MenuEntry[] menuEntries = client.getMenuEntries();
MenuEntry lastEntry = menuEntries[menuEntries.length - 1];
final WorldPoint worldPoint = WorldPoint.fromScene(client, sceneX, sceneY, client.getPlane());
GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(itemId, worldPoint);
GroundItem groundItem = collectedGroundItems.get(groundItemKey);
int quantity = groundItem.getQuantity();
final int gePrice = groundItem.getGePrice();
final int haPrice = groundItem.getHaPrice();
final Color hidden = getHidden(new NamedQuantity(groundItem.getName(), quantity), gePrice, haPrice, groundItem.isTradeable());
final Color highlighted = getHighlighted(new NamedQuantity(groundItem.getName(), quantity), gePrice, haPrice);
final Color color = getItemColor(highlighted, hidden);
final boolean canBeRecolored = highlighted != null || (hidden != null && config.recolorMenuHiddenItems());
if (color != null && canBeRecolored && !color.equals(config.defaultColor()))
{
final MenuHighlightMode mode = config.menuHighlightMode();
if (mode == BOTH || mode == OPTION)
{
final String optionText = telegrabEntry ? "Cast" : "Take";
lastEntry.setOption(ColorUtil.prependColorTag(optionText, color));
}
if (mode == BOTH || mode == NAME)
{
String target = lastEntry.getTarget();
if (telegrabEntry)
{
target = target.substring(TELEGRAB_TEXT.length());
}
target = ColorUtil.prependColorTag(target.substring(target.indexOf('>') + 1), color);
if (telegrabEntry)
{
target = TELEGRAB_TEXT + target;
}
lastEntry.setTarget(target);
}
}
if (config.showMenuItemQuantities() && groundItem.isStackable() && quantity > 1)
{
lastEntry.setTarget(lastEntry.getTarget() + " (" + quantity + ")");
}
client.setMenuEntries(menuEntries);
}
}
void updateList(String item, boolean hiddenList)
{
final List<String> hiddenItemSet = new ArrayList<>(hiddenItemList);
final List<String> highlightedItemSet = new ArrayList<>(highlightedItemsList);
if (hiddenList)
{
highlightedItemSet.removeIf(item::equalsIgnoreCase);
}
else
{
hiddenItemSet.removeIf(item::equalsIgnoreCase);
}
final List<String> items = hiddenList ? hiddenItemSet : highlightedItemSet;
if (!items.removeIf(item::equalsIgnoreCase))
{
items.add(item);
}
config.setHiddenItems(Text.toCSV(hiddenItemSet));
config.setHighlightedItem(Text.toCSV(highlightedItemSet));
}
Color getHighlighted(NamedQuantity item, int gePrice, int haPrice)
{
if (TRUE.equals(highlightedItems.getUnchecked(item)))
{
return config.highlightedColor();
}
// Explicit hide takes priority over implicit highlight
if (TRUE.equals(hiddenItems.getUnchecked(item)))
{
return null;
}
final int price = getValueByMode(gePrice, haPrice);
for (PriceHighlight highlight : priceChecks)
{
if (price > highlight.getPrice())
{
return highlight.getColor();
}
}
return null;
}
Color getHidden(NamedQuantity item, int gePrice, int haPrice, boolean isTradeable)
{
final boolean isExplicitHidden = TRUE.equals(hiddenItems.getUnchecked(item));
final boolean isExplicitHighlight = TRUE.equals(highlightedItems.getUnchecked(item));
final boolean canBeHidden = gePrice > 0 || isTradeable || !config.dontHideUntradeables();
final boolean underGe = gePrice < config.getHideUnderValue();
final boolean underHa = haPrice < config.getHideUnderValue();
// Explicit highlight takes priority over implicit hide
return isExplicitHidden || (!isExplicitHighlight && canBeHidden && underGe && underHa)
? config.hiddenColor()
: null;
}
Color getItemColor(Color highlighted, Color hidden)
{
if (highlighted != null)
{
return highlighted;
}
if (hidden != null)
{
return hidden;
}
return config.defaultColor();
}
@Subscribe
public void onFocusChanged(FocusChanged focusChanged)
{
if (!focusChanged.isFocused())
{
setHotKeyPressed(false);
}
}
private void notifyHighlightedItem(GroundItem item)
{
final boolean shouldNotifyHighlighted = config.notifyHighlightedDrops() &&
TRUE.equals(highlightedItems.getUnchecked(new NamedQuantity(item)));
final boolean shouldNotifyTier = config.notifyTier() != HighlightTier.OFF &&
getValueByMode(item.getGePrice(), item.getHaPrice()) > config.notifyTier().getValueFromTier(config) &&
FALSE.equals(hiddenItems.getUnchecked(new NamedQuantity(item)));
final String dropType;
if (shouldNotifyHighlighted)
{
dropType = "highlighted";
}
else if (shouldNotifyTier)
{
dropType = "valuable";
}
else
{
return;
}
final Player local = client.getLocalPlayer();
final StringBuilder notificationStringBuilder = new StringBuilder()
.append('[')
.append(local.getName())
.append("] received a ")
.append(dropType)
.append(" drop: ")
.append(item.getName());
if (item.getQuantity() > 1)
{
if (item.getQuantity() >= MAX_QUANTITY)
{
notificationStringBuilder.append(" (Lots!)");
}
else
{
notificationStringBuilder.append(" (")
.append(QuantityFormatter.quantityToStackSize(item.getQuantity()))
.append(")");
}
}
notifier.notify(notificationStringBuilder.toString());
}
private int getValueByMode(int gePrice, int haPrice)
{
switch (config.valueCalculationMode())
{
case GE:
return gePrice;
case HA:
return haPrice;
default: // Highest
return Math.max(gePrice, haPrice);
}
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
{
if (menuOptionClicked.getMenuAction() == MenuAction.ITEM_DROP)
{
int itemId = menuOptionClicked.getId();
// Keep a queue of recently dropped items to better detect
// item spawns that are drops
droppedItemQueue.add(itemId);
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2020, dekvall <https://github.com/dekvall>
* 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.grounditems;
import com.google.common.base.Strings;
import lombok.Value;
@Value
class ItemThreshold
{
enum Inequality
{
LESS_THAN,
MORE_THAN
}
private final String itemName;
private final int quantity;
private final Inequality inequality;
static ItemThreshold fromConfigEntry(String entry)
{
if (Strings.isNullOrEmpty(entry))
{
return null;
}
Inequality operator = Inequality.MORE_THAN;
int qty = 0;
for (int i = entry.length() - 1; i >= 0; i--)
{
char c = entry.charAt(i);
if (c >= '0' && c <= '9' || Character.isWhitespace(c))
{
continue;
}
switch (c)
{
case '<':
operator = Inequality.LESS_THAN;
// fallthrough
case '>':
if (i + 1 < entry.length())
{
try
{
qty = Integer.parseInt(entry.substring(i + 1).trim());
}
catch (NumberFormatException e)
{
qty = 0;
operator = Inequality.MORE_THAN;
}
entry = entry.substring(0, i);
}
}
break;
}
return new ItemThreshold(entry.trim(), qty, operator);
}
boolean quantityHolds(int itemCount)
{
if (inequality == Inequality.LESS_THAN)
{
return itemCount < quantity;
}
else
{
return itemCount > quantity;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* 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.grounditems;
enum LootType
{
UNKNOWN,
DROPPED,
PVP,
PVM;
}

Some files were not shown because too many files have changed in this diff Show More