Files
runelite/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java

559 lines
16 KiB
Java

/*
* 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.game;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.reactivex.schedulers.Schedulers;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.Constants;
import static net.runelite.api.Constants.CLIENT_DEFAULT_ZOOM;
import static net.runelite.api.Constants.HIGH_ALCHEMY_MULTIPLIER;
import net.runelite.api.GameState;
import net.runelite.api.ItemDefinition;
import net.runelite.api.ItemID;
import static net.runelite.api.ItemID.*;
import net.runelite.api.Sprite;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.PostItemDefinition;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.eventbus.EventBus;
import net.runelite.http.api.item.ItemClient;
import net.runelite.http.api.item.ItemPrice;
import net.runelite.http.api.item.ItemStats;
import org.jetbrains.annotations.NotNull;
@Singleton
@Slf4j
public class ItemManager
{
// Worn items with weight reducing property have a different worn and inventory ItemID
private static final ImmutableMap<Integer, Integer> WORN_ITEMS = ImmutableMap.<Integer, Integer>builder().
put(BOOTS_OF_LIGHTNESS_89, BOOTS_OF_LIGHTNESS).
put(PENANCE_GLOVES_10554, PENANCE_GLOVES).
put(GRACEFUL_HOOD_11851, GRACEFUL_HOOD).
put(GRACEFUL_CAPE_11853, GRACEFUL_CAPE).
put(GRACEFUL_TOP_11855, GRACEFUL_TOP).
put(GRACEFUL_LEGS_11857, GRACEFUL_LEGS).
put(GRACEFUL_GLOVES_11859, GRACEFUL_GLOVES).
put(GRACEFUL_BOOTS_11861, GRACEFUL_BOOTS).
put(GRACEFUL_HOOD_13580, GRACEFUL_HOOD_13579).
put(GRACEFUL_CAPE_13582, GRACEFUL_CAPE_13581).
put(GRACEFUL_TOP_13584, GRACEFUL_TOP_13583).
put(GRACEFUL_LEGS_13586, GRACEFUL_LEGS_13585).
put(GRACEFUL_GLOVES_13588, GRACEFUL_GLOVES_13587).
put(GRACEFUL_BOOTS_13590, GRACEFUL_BOOTS_13589).
put(GRACEFUL_HOOD_13592, GRACEFUL_HOOD_13591).
put(GRACEFUL_CAPE_13594, GRACEFUL_CAPE_13593).
put(GRACEFUL_TOP_13596, GRACEFUL_TOP_13595).
put(GRACEFUL_LEGS_13598, GRACEFUL_LEGS_13597).
put(GRACEFUL_GLOVES_13600, GRACEFUL_GLOVES_13599).
put(GRACEFUL_BOOTS_13602, GRACEFUL_BOOTS_13601).
put(GRACEFUL_HOOD_13604, GRACEFUL_HOOD_13603).
put(GRACEFUL_CAPE_13606, GRACEFUL_CAPE_13605).
put(GRACEFUL_TOP_13608, GRACEFUL_TOP_13607).
put(GRACEFUL_LEGS_13610, GRACEFUL_LEGS_13609).
put(GRACEFUL_GLOVES_13612, GRACEFUL_GLOVES_13611).
put(GRACEFUL_BOOTS_13614, GRACEFUL_BOOTS_13613).
put(GRACEFUL_HOOD_13616, GRACEFUL_HOOD_13615).
put(GRACEFUL_CAPE_13618, GRACEFUL_CAPE_13617).
put(GRACEFUL_TOP_13620, GRACEFUL_TOP_13619).
put(GRACEFUL_LEGS_13622, GRACEFUL_LEGS_13621).
put(GRACEFUL_GLOVES_13624, GRACEFUL_GLOVES_13623).
put(GRACEFUL_BOOTS_13626, GRACEFUL_BOOTS_13625).
put(GRACEFUL_HOOD_13628, GRACEFUL_HOOD_13627).
put(GRACEFUL_CAPE_13630, GRACEFUL_CAPE_13629).
put(GRACEFUL_TOP_13632, GRACEFUL_TOP_13631).
put(GRACEFUL_LEGS_13634, GRACEFUL_LEGS_13633).
put(GRACEFUL_GLOVES_13636, GRACEFUL_GLOVES_13635).
put(GRACEFUL_BOOTS_13638, GRACEFUL_BOOTS_13637).
put(GRACEFUL_HOOD_13668, GRACEFUL_HOOD_13667).
put(GRACEFUL_CAPE_13670, GRACEFUL_CAPE_13669).
put(GRACEFUL_TOP_13672, GRACEFUL_TOP_13671).
put(GRACEFUL_LEGS_13674, GRACEFUL_LEGS_13673).
put(GRACEFUL_GLOVES_13676, GRACEFUL_GLOVES_13675).
put(GRACEFUL_BOOTS_13678, GRACEFUL_BOOTS_13677).
put(GRACEFUL_HOOD_21063, GRACEFUL_HOOD_21061).
put(GRACEFUL_CAPE_21066, GRACEFUL_CAPE_21064).
put(GRACEFUL_TOP_21069, GRACEFUL_TOP_21067).
put(GRACEFUL_LEGS_21072, GRACEFUL_LEGS_21070).
put(GRACEFUL_GLOVES_21075, GRACEFUL_GLOVES_21073).
put(GRACEFUL_BOOTS_21078, GRACEFUL_BOOTS_21076).
put(MAX_CAPE_13342, MAX_CAPE).
put(SPOTTED_CAPE_10073, SPOTTED_CAPE).
put(SPOTTIER_CAPE_10074, SPOTTIER_CAPE).
put(AGILITY_CAPET_13341, AGILITY_CAPET).
put(AGILITY_CAPE_13340, AGILITY_CAPE).
build();
private final Client client;
private final ScheduledExecutorService scheduledExecutorService;
private final ClientThread clientThread;
private final ItemClient itemClient;
private final ImmutableMap<Integer, ItemStats> itemStatMap;
private final LoadingCache<ImageKey, AsyncBufferedImage> itemImages;
private final LoadingCache<Integer, ItemDefinition> itemDefinitions;
private final LoadingCache<OutlineKey, BufferedImage> itemOutlines;
private Map<Integer, ItemPrice> itemPrices = Collections.emptyMap();
private Map<Integer, ItemStats> itemStats = Collections.emptyMap();
@Inject
public ItemManager(
Client client,
ScheduledExecutorService executor,
ClientThread clientThread,
EventBus eventbus,
ItemClient itemClient
)
{
this.client = client;
this.scheduledExecutorService = executor;
this.clientThread = clientThread;
this.itemClient = itemClient;
scheduledExecutorService.scheduleWithFixedDelay(this::loadPrices, 0, 30, TimeUnit.MINUTES);
scheduledExecutorService.submit(this::loadStats);
itemImages = CacheBuilder.newBuilder()
.maximumSize(128L)
.expireAfterAccess(1, TimeUnit.HOURS)
.build(new CacheLoader<ImageKey, AsyncBufferedImage>()
{
@Override
public AsyncBufferedImage load(@NotNull ImageKey key) throws Exception
{
return loadImage(key.itemId, key.itemQuantity, key.stackable);
}
});
itemDefinitions = CacheBuilder.newBuilder()
.maximumSize(1024L)
.expireAfterAccess(1, TimeUnit.HOURS)
.build(new CacheLoader<Integer, ItemDefinition>()
{
@Override
public ItemDefinition load(@NotNull Integer key) throws Exception
{
return client.getItemDefinition(key);
}
});
itemOutlines = CacheBuilder.newBuilder()
.maximumSize(128L)
.expireAfterAccess(1, TimeUnit.HOURS)
.build(new CacheLoader<OutlineKey, BufferedImage>()
{
@Override
public BufferedImage load(@NotNull OutlineKey key) throws Exception
{
return loadItemOutline(key.itemId, key.itemQuantity, key.outlineColor);
}
});
final Gson gson = new Gson();
final Type typeToken = new TypeToken<Map<Integer, ItemStats>>()
{
}.getType();
final InputStream statsFile = getClass().getResourceAsStream("/item_stats.json");
final Map<Integer, ItemStats> stats = gson.fromJson(new InputStreamReader(statsFile), typeToken);
itemStatMap = ImmutableMap.copyOf(stats);
eventbus.subscribe(GameStateChanged.class, this, this::onGameStateChanged);
eventbus.subscribe(PostItemDefinition.class, this, this::onPostItemDefinition);
}
private void loadPrices()
{
itemClient.getPrices()
.subscribeOn(Schedulers.io())
.subscribe(
(prices) ->
{
if (prices != null)
{
ImmutableMap.Builder<Integer, ItemPrice> map = ImmutableMap.builderWithExpectedSize(prices.length);
for (ItemPrice price : prices)
{
map.put(price.getId(), price);
}
itemPrices = map.build();
}
log.debug("Loaded {} prices", itemPrices.size());
},
(e) -> log.warn("error loading prices!", e)
);
}
private void loadStats()
{
itemClient.getStats()
.subscribeOn(Schedulers.io())
.subscribe(
(stats) ->
{
if (stats != null)
{
itemStats = ImmutableMap.copyOf(stats);
}
log.debug("Loaded {} stats", itemStats.size());
},
(e) -> log.warn("error loading stats!", e)
);
}
private void onGameStateChanged(final GameStateChanged event)
{
if (event.getGameState() == GameState.HOPPING || event.getGameState() == GameState.LOGIN_SCREEN)
{
itemDefinitions.invalidateAll();
}
}
private void onPostItemDefinition(PostItemDefinition event)
{
itemDefinitions.put(event.getItemDefinition().getId(), event.getItemDefinition());
}
/**
* Invalidates internal item manager item composition cache (but not client item composition cache)
*
* @see Client#getItemDefinitionCache()
*/
public void invalidateItemDefinitionCache()
{
itemDefinitions.invalidateAll();
}
/**
* Look up an item's price
*
* @param itemID item id
* @return item price
*/
public int getItemPrice(int itemID)
{
return getItemPrice(itemID, false);
}
/**
* Look up an item's price
*
* @param itemID item id
* @param ignoreUntradeableMap should the price returned ignore the {@link UntradeableItemMapping}
* @return item price
*/
public int getItemPrice(int itemID, boolean ignoreUntradeableMap)
{
if (itemID == ItemID.COINS_995)
{
return 1;
}
if (itemID == ItemID.PLATINUM_TOKEN)
{
return 1000;
}
if (!ignoreUntradeableMap)
{
UntradeableItemMapping p = UntradeableItemMapping.map(ItemVariationMapping.map(itemID));
if (p != null)
{
return getItemPrice(p.getPriceID()) * p.getQuantity();
}
}
int price = 0;
for (int mappedID : ItemMapping.map(itemID))
{
ItemPrice ip = itemPrices.get(mappedID);
if (ip != null)
{
price += ip.getPrice();
}
}
return price;
}
public int getAlchValue(ItemDefinition composition)
{
if (composition.getId() == ItemID.COINS_995)
{
return 1;
}
if (composition.getId() == ItemID.PLATINUM_TOKEN)
{
return 1000;
}
return (int) Math.max(1, composition.getPrice() * HIGH_ALCHEMY_MULTIPLIER);
}
public int getAlchValue(int itemID)
{
if (itemID == ItemID.COINS_995)
{
return 1;
}
if (itemID == ItemID.PLATINUM_TOKEN)
{
return 1000;
}
return (int) Math.max(1, getItemDefinition(itemID).getPrice() * HIGH_ALCHEMY_MULTIPLIER);
}
public int getBrokenValue(int itemId)
{
PvPValueBrokenItem b = PvPValueBrokenItem.of(itemId);
if (b != null)
{
return (int) (b.getValue() * (75.0f / 100.0f));
}
return 0;
}
/**
* Look up an item's stats
*
* @param itemId item id
* @return item stats
*/
@Nullable
public ItemStats getItemStats(int itemId, boolean allowNote)
{
ItemDefinition itemDefinition = getItemDefinition(itemId);
if (itemDefinition.getName() == null || !allowNote && itemDefinition.getNote() != -1)
{
return null;
}
return itemStatMap.get(canonicalize(itemId));
}
/**
* Search for tradeable items based on item name
*
* @param itemName item name
* @return
*/
public List<ItemPrice> search(String itemName)
{
itemName = itemName.toLowerCase();
List<ItemPrice> result = new ArrayList<>();
for (ItemPrice itemPrice : itemPrices.values())
{
final String name = itemPrice.getName();
if (name.toLowerCase().contains(itemName))
{
result.add(itemPrice);
}
}
return result;
}
/**
* Look up an item's composition
*
* @param itemId item id
* @return item composition
*/
@Nonnull
public ItemDefinition getItemDefinition(int itemId)
{
assert client.isClientThread() : "getItemDefinition must be called on client thread";
return itemDefinitions.getUnchecked(itemId);
}
/**
* Get an item's un-noted, un-placeholdered ID
*/
public int canonicalize(int itemID)
{
ItemDefinition itemDefinition = getItemDefinition(itemID);
if (itemDefinition.getNote() != -1)
{
return itemDefinition.getLinkedNoteId();
}
if (itemDefinition.getPlaceholderTemplateId() != -1)
{
return itemDefinition.getPlaceholderId();
}
return WORN_ITEMS.getOrDefault(itemID, itemID);
}
/**
* Loads item sprite from game, makes transparent, and generates image
*
* @param itemId
* @return
*/
private AsyncBufferedImage loadImage(int itemId, int quantity, boolean stackable)
{
AsyncBufferedImage img = new AsyncBufferedImage(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT, BufferedImage.TYPE_INT_ARGB);
clientThread.invoke(() ->
{
if (client.getGameState().ordinal() < GameState.LOGIN_SCREEN.ordinal())
{
return false;
}
Sprite sprite = client.createItemSprite(itemId, quantity, 1, Sprite.DEFAULT_SHADOW_COLOR,
stackable ? 1 : 0, false, CLIENT_DEFAULT_ZOOM);
if (sprite == null)
{
return false;
}
sprite.toBufferedImage(img);
img.changed();
return true;
});
return img;
}
/**
* Get item sprite image as BufferedImage.
* <p>
* This method may return immediately with a blank image if not called on the game thread.
* The image will be filled in later. If this is used for a UI label/button, it should be added
* using AsyncBufferedImage::addTo to ensure it is painted properly
*
* @param itemId
* @return
*/
public AsyncBufferedImage getImage(int itemId)
{
return getImage(itemId, 1, false);
}
/**
* Get item sprite image as BufferedImage.
* <p>
* This method may return immediately with a blank image if not called on the game thread.
* The image will be filled in later. If this is used for a UI label/button, it should be added
* using AsyncBufferedImage::addTo to ensure it is painted properly
*
* @param itemId
* @param quantity
* @return
*/
public AsyncBufferedImage getImage(int itemId, int quantity, boolean stackable)
{
try
{
return itemImages.get(new ImageKey(itemId, quantity, stackable));
}
catch (ExecutionException ex)
{
return null;
}
}
/**
* Create item sprite and applies an outline.
*
* @param itemId item id
* @param itemQuantity item quantity
* @param outlineColor outline color
* @return image
*/
private BufferedImage loadItemOutline(final int itemId, final int itemQuantity, final Color outlineColor)
{
final Sprite itemSprite = client.createItemSprite(itemId, itemQuantity, 1, 0, 0, true, 710);
return itemSprite.toBufferedOutline(outlineColor);
}
/**
* Get item outline with a specific color.
*
* @param itemId item id
* @param itemQuantity item quantity
* @param outlineColor outline color
* @return image
*/
public BufferedImage getItemOutline(final int itemId, final int itemQuantity, final Color outlineColor)
{
try
{
return itemOutlines.get(new OutlineKey(itemId, itemQuantity, outlineColor));
}
catch (ExecutionException e)
{
return null;
}
}
@Value
private static class ImageKey
{
private final int itemId;
private final int itemQuantity;
private final boolean stackable;
}
@Value
private static class OutlineKey
{
private final int itemId;
private final int itemQuantity;
private final Color outlineColor;
}
}