From b3fa9a5e497a88c0f8addc5c727fb7e81f3d797b Mon Sep 17 00:00:00 2001 From: Max Weber Date: Wed, 11 Apr 2018 03:39:28 -0600 Subject: [PATCH] Allow ItemManager to be ran off the client thread This allows the ItemManager to return a blank BufferedImage if it's not called on the client thread. This also makes it not render the image until the cache has been downloaded --- .../java/net/runelite/api/SpritePixels.java | 7 ++ .../client/game/AsyncBufferedImage.java | 84 +++++++++++++++++++ .../net/runelite/client/game/ItemManager.java | 50 ++++++++--- .../runelite/mixins/RSSpritePixelsMixin.java | 19 ++++- 4 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/game/AsyncBufferedImage.java diff --git a/runelite-api/src/main/java/net/runelite/api/SpritePixels.java b/runelite-api/src/main/java/net/runelite/api/SpritePixels.java index 49fd5fc973..732ab1ffa0 100644 --- a/runelite-api/src/main/java/net/runelite/api/SpritePixels.java +++ b/runelite-api/src/main/java/net/runelite/api/SpritePixels.java @@ -44,4 +44,11 @@ public interface SpritePixels * @return */ BufferedImage toBufferedImage(); + + + /** + * Writes the contents of the SpritePixels to the BufferedImage. + * Width and Height must match + */ + void toBufferedImage(BufferedImage img); } diff --git a/runelite-client/src/main/java/net/runelite/client/game/AsyncBufferedImage.java b/runelite-client/src/main/java/net/runelite/client/game/AsyncBufferedImage.java new file mode 100644 index 0000000000..0290c7856a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/game/AsyncBufferedImage.java @@ -0,0 +1,84 @@ +/* + * 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.game; + +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; + +public class AsyncBufferedImage extends BufferedImage +{ + private final List listeners = new CopyOnWriteArrayList<>(); + public AsyncBufferedImage(int width, int height, int imageType) + { + super(width, height, imageType); + } + + /** + * Call when the buffer has been changed + */ + public void changed() + { + for (Runnable r : listeners) + { + r.run(); + } + } + + /** + * Register a function to be ran when the buffer has changed + */ + public void onChanged(Runnable r) + { + listeners.add(r); + } + + /** + * Calls setIcon on c, ensuring it is repainted when this changes + */ + public void addTo(JButton c) + { + c.setIcon(makeIcon(c)); + } + + /** + * Calls setIcon on c, ensuring it is repainted when this changes + */ + public void addTo(JLabel c) + { + c.setIcon(makeIcon(c)); + } + + private ImageIcon makeIcon(JComponent c) + { + listeners.add(c::repaint); + return new ImageIcon(this); + } +} 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 a29513e45b..950b0a4081 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 @@ -47,6 +47,7 @@ import net.runelite.api.GameState; import net.runelite.api.ItemComposition; import net.runelite.api.SpritePixels; import net.runelite.api.events.GameStateChanged; +import net.runelite.client.callback.ClientThread; import net.runelite.http.api.item.ItemClient; import net.runelite.http.api.item.ItemPrice; import net.runelite.http.api.item.SearchResult; @@ -75,18 +76,20 @@ public class ItemManager private final Client client; private final ScheduledExecutorService scheduledExecutorService; + private final ClientThread clientThread; private final ItemClient itemClient = new ItemClient(); private final LoadingCache itemSearches; private final LoadingCache itemPriceCache; - private final LoadingCache itemImages; + private final LoadingCache itemImages; private final LoadingCache itemCompositions; @Inject - public ItemManager(Client client, ScheduledExecutorService executor) + public ItemManager(Client client, ScheduledExecutorService executor, ClientThread clientThread) { this.client = client; this.scheduledExecutorService = executor; + this.clientThread = clientThread; itemPriceCache = CacheBuilder.newBuilder() .maximumSize(1024L) @@ -108,10 +111,10 @@ public class ItemManager itemImages = CacheBuilder.newBuilder() .maximumSize(128L) .expireAfterAccess(1, TimeUnit.HOURS) - .build(new CacheLoader() + .build(new CacheLoader() { @Override - public BufferedImage load(ImageKey key) throws Exception + public AsyncBufferedImage load(ImageKey key) throws Exception { return loadImage(key.itemId, key.itemQuantity, key.stackable); } @@ -271,32 +274,55 @@ public class ItemManager * @param itemId * @return */ - private BufferedImage loadImage(int itemId, int quantity, boolean stackable) + private AsyncBufferedImage loadImage(int itemId, int quantity, boolean stackable) { - SpritePixels sprite = client.createItemSprite(itemId, quantity, 1, SpritePixels.DEFAULT_SHADOW_COLOR, - stackable ? 1 : 0, false, CLIENT_DEFAULT_ZOOM); - return sprite.toBufferedImage(); + AsyncBufferedImage img = new AsyncBufferedImage(36, 32, BufferedImage.TYPE_INT_ARGB); + clientThread.invokeLater(() -> + { + if (client.getGameState().ordinal() < GameState.LOGIN_SCREEN.ordinal()) + { + return false; + } + SpritePixels sprite = client.createItemSprite(itemId, quantity, 1, SpritePixels.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 + * Get item sprite image as BufferedImage. + *

+ * 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 BufferedImage getImage(int itemId) + public AsyncBufferedImage getImage(int itemId) { return getImage(itemId, 1, false); } /** - * Get item sprite image as BufferedImage + * Get item sprite image as BufferedImage. + *

+ * 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 BufferedImage getImage(int itemId, int quantity, boolean stackable) + public AsyncBufferedImage getImage(int itemId, int quantity, boolean stackable) { try { diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSSpritePixelsMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSSpritePixelsMixin.java index eeca0febcb..7511df70d7 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSSpritePixelsMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSSpritePixelsMixin.java @@ -35,12 +35,28 @@ public abstract class RSSpritePixelsMixin implements RSSpritePixels @Inject @Override public BufferedImage toBufferedImage() + { + BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + + toBufferedImage(img); + + return img; + } + + @Inject + @Override + public void toBufferedImage(BufferedImage img) { int width = getWidth(); int height = getHeight(); + + if (img.getWidth() != width || img.getHeight() != height) + { + throw new IllegalArgumentException("Image bounds do not match SpritePixels"); + } + int[] pixels = getPixels(); int[] transPixels = new int[pixels.length]; - BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < pixels.length; i++) { @@ -51,6 +67,5 @@ public abstract class RSSpritePixelsMixin implements RSSpritePixels } img.setRGB(0, 0, width, height, transPixels, 0, width); - return img; } }