From ffb2f159735dfa166a6262b45b1e5fcacc62a75f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 6 Nov 2018 14:50:52 +0100 Subject: [PATCH] item stats: add support for equipment stats --- .../net/runelite/http/api/RuneLiteAPI.java | 6 + .../runelite/http/api/item/ItemClient.java | 37 ++++ .../http/api/item/ItemEquipmentStats.java | 54 ++++++ .../net/runelite/http/api/item/ItemStats.java | 82 +++++++++ .../net/runelite/client/game/ItemManager.java | 41 +++++ .../plugins/itemstats/ItemStatConfig.java | 23 ++- .../plugins/itemstats/ItemStatOverlay.java | 166 ++++++++++++++++-- 7 files changed, 394 insertions(+), 15 deletions(-) create mode 100644 http-api/src/main/java/net/runelite/http/api/item/ItemEquipmentStats.java create mode 100644 http-api/src/main/java/net/runelite/http/api/item/ItemStats.java diff --git a/http-api/src/main/java/net/runelite/http/api/RuneLiteAPI.java b/http-api/src/main/java/net/runelite/http/api/RuneLiteAPI.java index da35dc9e42..bc6721518d 100644 --- a/http-api/src/main/java/net/runelite/http/api/RuneLiteAPI.java +++ b/http-api/src/main/java/net/runelite/http/api/RuneLiteAPI.java @@ -44,6 +44,7 @@ public class RuneLiteAPI private static final String BASE = "https://api.runelite.net/runelite-"; private static final String WSBASE = "wss://api.runelite.net/runelite-"; + private static final String STATICBASE = "https://static.runelite.net"; private static final Properties properties = new Properties(); private static String version; private static int rsVersion; @@ -73,6 +74,11 @@ public class RuneLiteAPI return HttpUrl.parse(BASE + getVersion()); } + public static HttpUrl getStaticBase() + { + return HttpUrl.parse(STATICBASE); + } + public static String getWsEndpoint() { return WSBASE + getVersion() + "/ws"; diff --git a/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java b/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java index c72ce24e4b..93b138dc7f 100644 --- a/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java +++ b/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java @@ -25,11 +25,14 @@ package net.runelite.http.api.item; import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Type; import java.util.Arrays; +import java.util.Map; import javax.imageio.ImageIO; import net.runelite.http.api.RuneLiteAPI; import okhttp3.HttpUrl; @@ -200,4 +203,38 @@ public class ItemClient throw new IOException(ex); } } + + public Map getStats() throws IOException + { + HttpUrl.Builder urlBuilder = RuneLiteAPI.getStaticBase().newBuilder() + .addPathSegment("item") + .addPathSegment("stats.min.json"); + + HttpUrl url = urlBuilder.build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + logger.warn("Error looking up item stats: {}", response.message()); + return null; + } + + InputStream in = response.body().byteStream(); + final Type typeToken = new TypeToken>() + { + }.getType(); + return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), typeToken); + } + catch (JsonParseException ex) + { + throw new IOException(ex); + } + } } diff --git a/http-api/src/main/java/net/runelite/http/api/item/ItemEquipmentStats.java b/http-api/src/main/java/net/runelite/http/api/item/ItemEquipmentStats.java new file mode 100644 index 0000000000..5f7fdaccbe --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/item/ItemEquipmentStats.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.http.api.item; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class ItemEquipmentStats +{ + private int slot; + + private int astab; + private int aslash; + private int acrush; + private int amagic; + private int arange; + + private int dstab; + private int dslash; + private int dcrush; + private int dmagic; + private int drange; + + private int str; + private int rstr; + private int mdmg; + private int prayer; + private int aspeed; +} + diff --git a/http-api/src/main/java/net/runelite/http/api/item/ItemStats.java b/http-api/src/main/java/net/runelite/http/api/item/ItemStats.java new file mode 100644 index 0000000000..8ca22fd4f3 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/item/ItemStats.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.http.api.item; + +import lombok.Value; + +@Value +public class ItemStats +{ + private boolean quest; + private boolean equipable; + private double weight; + + private ItemEquipmentStats equipment; + + public ItemStats substract(ItemStats other) + { + if (other == null) + { + return this; + } + + final double newWeight = weight - other.weight; + final ItemEquipmentStats newEquipment; + + + if (other.equipment != null) + { + final ItemEquipmentStats equipment = this.equipment != null + ? this.equipment + : new ItemEquipmentStats.ItemEquipmentStatsBuilder().build(); + + newEquipment = new ItemEquipmentStats.ItemEquipmentStatsBuilder() + .slot(equipment.getSlot()) + .astab(equipment.getAstab() - other.equipment.getAstab()) + .aslash(equipment.getAslash() - other.equipment.getAslash()) + .acrush(equipment.getAcrush() - other.equipment.getAcrush()) + .amagic(equipment.getAmagic() - other.equipment.getAmagic()) + .arange(equipment.getArange() - other.equipment.getArange()) + .dstab(equipment.getDstab() - other.equipment.getDstab()) + .dslash(equipment.getDslash() - other.equipment.getDslash()) + .dcrush(equipment.getDcrush() - other.equipment.getDcrush()) + .dmagic(equipment.getDmagic() - other.equipment.getDmagic()) + .drange(equipment.getDrange() - other.equipment.getDrange()) + .str(equipment.getStr() - other.equipment.getStr()) + .rstr(equipment.getRstr() - other.equipment.getRstr()) + .mdmg(equipment.getMdmg() - other.equipment.getMdmg()) + .prayer(equipment.getPrayer() - other.equipment.getPrayer()) + .aspeed(equipment.getAspeed() - other.equipment.getAspeed()) + .build(); + } + else + { + newEquipment = equipment; + } + + return new ItemStats(quest, equipable, newWeight, newEquipment); + } +} + diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java b/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java index ed35e5f386..ad06024a7d 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java +++ b/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import lombok.Value; @@ -55,6 +56,7 @@ import net.runelite.client.callback.ClientThread; import net.runelite.client.eventbus.Subscribe; import net.runelite.http.api.item.ItemClient; import net.runelite.http.api.item.ItemPrice; +import net.runelite.http.api.item.ItemStats; @Singleton @Slf4j @@ -82,6 +84,7 @@ public class ItemManager private final ItemClient itemClient = new ItemClient(); private Map itemPrices = Collections.emptyMap(); + private Map itemStats = Collections.emptyMap(); private final LoadingCache itemImages; private final LoadingCache itemCompositions; private final LoadingCache itemOutlines; @@ -157,6 +160,7 @@ public class ItemManager this.clientThread = clientThread; scheduledExecutorService.scheduleWithFixedDelay(this::loadPrices, 0, 30, TimeUnit.MINUTES); + scheduledExecutorService.submit(this::loadStats); itemImages = CacheBuilder.newBuilder() .maximumSize(128L) @@ -218,6 +222,25 @@ public class ItemManager } } + private void loadStats() + { + try + { + final Map stats = itemClient.getStats(); + if (stats != null) + { + itemStats = ImmutableMap.copyOf(stats); + } + + log.debug("Loaded {} stats", itemStats.size()); + } + catch (IOException e) + { + log.warn("error loading stats!", e); + } + } + + @Subscribe public void onGameStateChanged(final GameStateChanged event) { @@ -269,6 +292,24 @@ public class ItemManager return price; } + /** + * Look up an item's stats + * @param itemId item id + * @return item stats + */ + @Nullable + public ItemStats getItemStats(int itemId) + { + ItemComposition itemComposition = getItemComposition(itemId); + + if (itemComposition == null || itemComposition.getName() == null) + { + return null; + } + + return itemStats.get(itemComposition.getName()); + } + /** * Search for tradeable items based on item name * diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatConfig.java index ec9d083044..c9fa8af82b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatConfig.java @@ -32,12 +32,31 @@ import net.runelite.client.config.ConfigItem; @ConfigGroup("itemstat") public interface ItemStatConfig extends Config { + @ConfigItem( + keyName = "consumableStats", + name = "Enable consumable stats", + description = "Enables tooltips for consumable items (food, boosts)" + ) + default boolean consumableStats() + { + return true; + } + + @ConfigItem( + keyName = "equipmentStats", + name = "Enable equipment stats", + description = "Enables tooltips for equipment items (combat bonuses, weight, prayer bonuses)" + ) + default boolean equipmentStats() + { + return true; + } + @ConfigItem( keyName = "relative", name = "Show Relative", description = "Show relative stat change in tooltip" ) - default boolean relative() { return true; @@ -48,7 +67,6 @@ public interface ItemStatConfig extends Config name = "Show Absolute", description = "Show absolute stat change in tooltip" ) - default boolean absolute() { return true; @@ -59,7 +77,6 @@ public interface ItemStatConfig extends Config name = "Show Theoretical", description = "Show theoretical stat change in tooltip" ) - default boolean theoretical() { return false; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java index 646116cc75..e67588db66 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java @@ -25,24 +25,31 @@ package net.runelite.client.plugins.itemstats; import com.google.inject.Inject; +import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import net.runelite.api.Client; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; import net.runelite.api.MenuEntry; +import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.JagexColors; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.tooltip.Tooltip; import net.runelite.client.ui.overlay.tooltip.TooltipManager; import net.runelite.client.util.ColorUtil; -import net.runelite.client.util.QueryRunner; +import net.runelite.http.api.item.ItemEquipmentStats; +import net.runelite.http.api.item.ItemStats; public class ItemStatOverlay extends Overlay { @Inject - private QueryRunner queryRunner; + private Client client; @Inject - private Client client; + private ItemManager itemManager; @Inject private TooltipManager tooltipManager; @@ -70,29 +77,164 @@ public class ItemStatOverlay extends Overlay } final MenuEntry entry = menu[menuSize - 1]; + final int group = WidgetInfo.TO_GROUP(entry.getParam1()); + final int child = WidgetInfo.TO_CHILD(entry.getParam1()); + final Widget widget = client.getWidget(group, child); - if (entry.getParam1() != WidgetInfo.INVENTORY.getId()) + if (widget == null || (group != WidgetInfo.INVENTORY.getGroupId() && + group != WidgetInfo.EQUIPMENT.getGroupId() && + group != WidgetInfo.EQUIPMENT_INVENTORY_ITEMS_CONTAINER.getGroupId())) { return null; } - final Effect change = statChanges.get(entry.getIdentifier()); - if (change != null) + int itemId = entry.getIdentifier(); + + if (group == WidgetInfo.EQUIPMENT.getGroupId()) { - final StringBuilder b = new StringBuilder(); - final StatsChanges statsChanges = change.calculate(client); - - for (final StatChange c : statsChanges.getStatChanges()) + final Widget widgetItem = widget.getChild(1); + if (widgetItem != null) { - b.append(buildStatChangeString(c)); + itemId = widgetItem.getItemId(); } + } + else if (group == WidgetInfo.EQUIPMENT_INVENTORY_ITEMS_CONTAINER.getGroupId()) + { + final Widget widgetItem = widget.getChild(entry.getParam0()); + if (widgetItem != null) + { + itemId = widgetItem.getItemId(); + } + } - tooltipManager.add(new Tooltip(b.toString())); + if (config.consumableStats()) + { + final Effect change = statChanges.get(itemId); + if (change != null) + { + final StringBuilder b = new StringBuilder(); + final StatsChanges statsChanges = change.calculate(client); + + for (final StatChange c : statsChanges.getStatChanges()) + { + b.append(buildStatChangeString(c)); + } + + final String tooltip = b.toString(); + + if (!tooltip.isEmpty()) + { + tooltipManager.add(new Tooltip(tooltip)); + } + } + } + + if (config.equipmentStats()) + { + final ItemStats stats = itemManager.getItemStats(itemId); + + if (stats != null) + { + final String tooltip = buildStatBonusString(stats); + + if (!tooltip.isEmpty()) + { + tooltipManager.add(new Tooltip(tooltip)); + } + } } return null; } + private String getChangeString( + final String label, + final double value, + final boolean inverse, + final boolean showPercent) + { + final Color plus = Positivity.getColor(config, Positivity.BETTER_UNCAPPED); + final Color minus = Positivity.getColor(config, Positivity.WORSE); + + if (value == 0) + { + return ""; + } + + final Color color; + + if (inverse) + { + color = value > 0 ? minus : plus; + } + else + { + color = value > 0 ? plus : minus; + } + + final String prefix = value > 0 ? "+" : ""; + final String suffix = showPercent ? "%" : ""; + final String valueString = (int)value == value ? String.valueOf((int)value) : String.valueOf(value); + return label + ": " + ColorUtil.wrapWithColorTag(prefix + valueString + suffix, color) + "
"; + } + + private String buildStatBonusString(ItemStats s) + { + final StringBuilder b = new StringBuilder(); + b.append(getChangeString("Weight", s.getWeight(), true, false)); + + ItemStats other = null; + final ItemEquipmentStats currentEquipment = s.getEquipment(); + + if (s.isEquipable() && currentEquipment != null) + { + final Item[] items = client.getItemContainer(InventoryID.EQUIPMENT).getItems(); + + if (currentEquipment.getSlot() != -1 && currentEquipment.getSlot() < items.length) + { + final Item item = items[currentEquipment.getSlot()]; + if (item != null) + { + other = itemManager.getItemStats(item.getId()); + } + } + } + + final ItemStats substracted = s.substract(other); + final ItemEquipmentStats e = substracted.getEquipment(); + + if (substracted.isEquipable() && e != null) + { + b.append(getChangeString("Prayer", e.getPrayer(), false, false)); + b.append(getChangeString("Speed", e.getAspeed(), false, false)); + b.append(getChangeString("Melee Str", e.getStr(), false, false)); + b.append(getChangeString("Range Str", e.getRstr(), false, false)); + b.append(getChangeString("Magic Dmg", e.getMdmg(), false, true)); + + if (e.getAstab() != 0 || e.getAslash() != 0 || e.getAcrush() != 0 || e.getAmagic() != 0 || e.getArange() != 0) + { + b.append(ColorUtil.wrapWithColorTag("Attack Bonus
", JagexColors.MENU_TARGET)); + b.append(getChangeString("Stab", e.getAstab(), false, false)); + b.append(getChangeString("Slash", e.getAslash(), false, false)); + b.append(getChangeString("Crush", e.getAcrush(), false, false)); + b.append(getChangeString("Magic", e.getAmagic(), false, false)); + b.append(getChangeString("Range", e.getArange(), false, false)); + } + + if (e.getDstab() != 0 || e.getDslash() != 0 || e.getDcrush() != 0 || e.getDmagic() != 0 || e.getDrange() != 0) + { + b.append(ColorUtil.wrapWithColorTag("Defence Bonus
", JagexColors.MENU_TARGET)); + b.append(getChangeString("Stab", e.getDstab(), false, false)); + b.append(getChangeString("Slash", e.getDslash(), false, false)); + b.append(getChangeString("Crush", e.getDcrush(), false, false)); + b.append(getChangeString("Magic", e.getDmagic(), false, false)); + b.append(getChangeString("Range", e.getDrange(), false, false)); + } + } + + return b.toString(); + } + private String buildStatChangeString(StatChange c) { StringBuilder b = new StringBuilder();