diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java index a202d3baf9..0cfc81ca28 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -1819,4 +1819,24 @@ public interface Client extends GameShell void setSelectedSpellWidget(int widgetID); void setSelectedSpellChildIndex(int index); + + /** + * Scales values from pixels onto canvas + * + * @see net.runelite.client.util.ImageUtil#resizeSprite(Client, Sprite, int, int) + * + * @param canvas the array we're writing to + * @param pixels pixels to draw + * @param color should be 0 + * @param pixelX x index + * @param pixelY y index + * @param canvasIdx index in canvas (canvas[canvasIdx]) + * @param canvasOffset x offset + * @param newWidth new width + * @param newHeight new height + * @param pixelWidth pretty much horizontal scale + * @param pixelHeight pretty much vertical scale + * @param oldWidth old width + */ + void scaleSprite(int[] canvas, int[] pixels, int color, int pixelX, int pixelY, int canvasIdx, int canvasOffset, int newWidth, int newHeight, int pixelWidth, int pixelHeight, int oldWidth); } \ No newline at end of file diff --git a/runelite-api/src/main/java/net/runelite/api/Sprite.java b/runelite-api/src/main/java/net/runelite/api/Sprite.java index b6fed1bab4..2feadb6012 100644 --- a/runelite-api/src/main/java/net/runelite/api/Sprite.java +++ b/runelite-api/src/main/java/net/runelite/api/Sprite.java @@ -92,4 +92,20 @@ public interface Sprite * @param color target color */ void toBufferedOutline(BufferedImage img, int color); + + int getMaxWidth(); + + void setMaxWidth(int maxWidth); + + int getMaxHeight(); + + void setMaxHeight(int maxHeight); + + int getOffsetX(); + + void setOffsetX(int offsetX); + + int getOffsetY(); + + void setOffsetY(int offsetY); } diff --git a/runelite-api/src/main/java/net/runelite/api/SpriteID.java b/runelite-api/src/main/java/net/runelite/api/SpriteID.java index d2667b2e85..cde73ba3f8 100644 --- a/runelite-api/src/main/java/net/runelite/api/SpriteID.java +++ b/runelite-api/src/main/java/net/runelite/api/SpriteID.java @@ -1573,6 +1573,8 @@ public final class SpriteID /* Unmapped: 1709, 1710 */ public static final int TAB_MAGIC_SPELLBOOK_ARCEUUS = 1711; public static final int BIG_ASS_GUTHIX_SPELL = 1774; + public static final int BIG_ASS_GREY_ENTANGLE = 1788; + public static final int BIG_ASS_WHITE_ENTANGLE = 1789; public static final int BIG_SUPERHEAT = 1800; public static final int BIG_SPEC_TRANSFER = 1959; /* Unmapped: 1712~2175 */ diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java index f544845e8c..6056be36df 100644 --- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java +++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java @@ -53,7 +53,13 @@ public enum VarPlayer IN_RAID_PARTY(1427), NMZ_REWARD_POINTS(1060), - + + /** + * The 11 least significant bits of this var correspond to the player + * you're currently fighting. Value is -1 when not fighting any player. + * + * Client.getVar(ATTACKING_PLAYER) & 2047 == Client.getLocalInteractingIndex(); + */ ATTACKING_PLAYER(1075), /** diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java index acd9c7a78a..a367fee0c4 100644 --- a/runelite-api/src/main/java/net/runelite/api/Varbits.java +++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java @@ -688,7 +688,9 @@ public enum Varbits */ GAUNTLET_ENTERED(9178), - WITHDRAW_X_AMOUNT(3960); + WITHDRAW_X_AMOUNT(3960), + + IN_PVP_AREA(8121); /** * The raw varbit ID. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraConfig.java new file mode 100644 index 0000000000..4cf308f3e7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraConfig.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2019, Lucas + * 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.alchemicalhydra; + +import java.awt.Color; +import net.runelite.client.config.Alpha; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; + +@ConfigGroup("betterHydra") +public interface HydraConfig extends Config +{ + @ConfigSection( + keyName = "features", + name = "Features", + description = "Feathers. Jk, features", + position = 0 + ) + default boolean features() + { + return true; + } + + @ConfigItem( + keyName = "counting", + name = "Prayer helper", + description = "Basically everything this plugin is known for. Also has attacks between specs and poison overlay. Shouldn't NOT use this tbh", + position = 1, + section = "features" + ) + default boolean counting() + { + return true; + } + + @ConfigItem( + keyName = "fountain", + name = "Fountain helper", + description = "Indicates if hydra is on a fountain", + position = 2, + section = "features" + ) + default boolean fountain() + { + return true; + } + + @ConfigItem( + keyName = "stun", + name = "Stun timer", + description = "Shows when you can walk in fire phase", + position = 3, + section = "features" + ) + default boolean stun() + { + return false; + } + + @ConfigSection( + keyName = "colours", + name = "Colours", + description = "colours...", + position = 2 + ) + default boolean colours() + { + return false; + } + + @Alpha + @ConfigItem( + keyName = "safeCol", + name = "Safe colour", + description = "Colour overlay will be when there's >2 attacks left", + position = 1, + section = "colours" + ) + default Color safeCol() + { + return new Color(0, 156, 0, 156); + } + + @Alpha + @ConfigItem( + keyName = "medCol", + name = "Medium colour", + description = "Colour overlay will be when a input is coming up", + position = 2, + section = "colours" + ) + default Color medCol() + { + return new Color(200, 156, 0, 156); + } + + @Alpha + @ConfigItem( + keyName = "badCol", + name = "Bad colour", + description = "Colour overlay will be when you have to do something NOW", + position = 3, + section = "colours" + ) + default Color badCol() + { + return new Color(156, 0, 0, 156); + } + + @Alpha + @ConfigItem( + keyName = "poisonBorderCol", + name = "Poison border colour", + description = "Colour the edges of the area highlighted by poison special will be", + position = 4, + section = "colours" + ) + default Color poisonBorderCol() + { + return new Color(255, 0, 0, 100); + } + + @Alpha + @ConfigItem( + keyName = "poisonCol", + name = "Poison colour", + description = "Colour the fill of the area highlighted by poison special will be", + position = 5, + section = "colours" + ) + default Color poisonCol() + { + return new Color(255, 0, 0, 50); + } + + @Alpha + @ConfigItem( + keyName = "fountainColA", + name = "Fountain colour (not on top)", + description = "Fountain colour (not on top)", + position = 6, + section = "colours" + ) + default Color fountainColA() + { + return new Color(255, 0, 0, 100); + } + + @Alpha + @ConfigItem( + keyName = "fountainColB", + name = "Fountain colour (on top)", + description = "Fountain colour (on top)", + position = 7, + section = "colours" + ) + default Color fountainColB() + { + return new Color(0, 255, 0, 100); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraOverlay.java index a84ec94887..38572c9ee2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraOverlay.java @@ -28,24 +28,27 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; +import java.awt.image.BufferedImage; import javax.inject.Inject; import javax.inject.Singleton; +import lombok.AccessLevel; +import lombok.Setter; import net.runelite.api.Client; +import net.runelite.api.IndexDataBase; import net.runelite.api.Prayer; +import net.runelite.api.Sprite; +import net.runelite.api.SpriteID; import net.runelite.client.game.SpriteManager; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.components.ComponentOrientation; import net.runelite.client.ui.overlay.components.InfoBoxComponent; import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.util.ImageUtil; @Singleton class HydraOverlay extends Overlay { - - private static final Color RED_BG_COL = new Color(156, 0, 0, 156); - private static final Color YEL_BG_COL = new Color(200, 156, 0, 156); - private static final Color GRN_BG_COL = new Color(0, 156, 0, 156); static final int IMGSIZE = 36; private final HydraPlugin plugin; @@ -53,50 +56,104 @@ class HydraOverlay extends Overlay private final SpriteManager spriteManager; private final PanelComponent panelComponent = new PanelComponent(); + private BufferedImage stunImg; + + @Setter(AccessLevel.PACKAGE) + private Color safeCol; + + @Setter(AccessLevel.PACKAGE) + private Color medCol; + + @Setter(AccessLevel.PACKAGE) + private Color badCol; + + @Setter(AccessLevel.PACKAGE) + private int stunTicks; + @Inject HydraOverlay(final HydraPlugin plugin, final Client client, final SpriteManager spriteManager) { this.plugin = plugin; this.client = client; this.spriteManager = spriteManager; - setPosition(OverlayPosition.BOTTOM_RIGHT); + this.setPosition(OverlayPosition.BOTTOM_RIGHT); panelComponent.setOrientation(ComponentOrientation.VERTICAL); } @Override public Dimension render(Graphics2D graphics2D) { - Hydra hydra = plugin.getHydra(); + final Hydra hydra = plugin.getHydra(); panelComponent.getChildren().clear(); - if (hydra == null || client == null) + if (hydra == null) { return null; } - //Add spec overlay first, to keep it above pray + // First add stunned thing if needed + if (stunTicks > 0) + { + addStunOverlay(); + } + + + if (plugin.isCounting()) + { + // Add spec box second, to keep it above pray + addSpecOverlay(hydra); + + // Finally add prayer box + addPrayOverlay(hydra); + } + + panelComponent.setPreferredSize(new Dimension(40, 0)); + panelComponent.setBorder(new Rectangle(0, 0, 0, 0)); + + return panelComponent.render(graphics2D); + } + + private void addStunOverlay() + { + final InfoBoxComponent stunComponent = new InfoBoxComponent(); + + stunComponent.setBackgroundColor(badCol); + stunComponent.setImage(getStunImg()); + stunComponent.setText(" " + stunTicks); + stunComponent.setPreferredSize(new Dimension(40, 40)); + + panelComponent.getChildren().add(stunComponent); + } + + private void addSpecOverlay(final Hydra hydra) + { final HydraPhase phase = hydra.getPhase(); final int nextSpec = hydra.getNextSpecialRelative(); - if (nextSpec <= 3) + if (nextSpec > 3) { - InfoBoxComponent specComponent = new InfoBoxComponent(); + return; + } + final InfoBoxComponent specComponent = new InfoBoxComponent(); - if (nextSpec == 0) - { - specComponent.setBackgroundColor(RED_BG_COL); - } - else if (nextSpec == 1) - { - specComponent.setBackgroundColor(YEL_BG_COL); - } - - specComponent.setImage(phase.getSpecImage(spriteManager)); - specComponent.setText(" " + (nextSpec)); //hacky way to not have to figure out how to move text - specComponent.setPreferredSize(new Dimension(40, 40)); - panelComponent.getChildren().add(specComponent); + if (nextSpec == 0) + { + specComponent.setBackgroundColor(badCol); + } + else if (nextSpec == 1) + { + specComponent.setBackgroundColor(medCol); } + specComponent.setImage(phase.getSpecImage(spriteManager)); + specComponent.setText(" " + nextSpec); // hacky way to not have to figure out how to move text + specComponent.setPreferredSize(new Dimension(40, 40)); + + panelComponent.getChildren().add(specComponent); + } + + private void addPrayOverlay(final Hydra hydra) + { final Prayer nextPrayer = hydra.getNextAttack().getPrayer(); final int nextSwitch = hydra.getNextSwitch(); @@ -104,21 +161,65 @@ class HydraOverlay extends Overlay if (nextSwitch == 1) { - prayComponent.setBackgroundColor(client.isPrayerActive(nextPrayer) ? YEL_BG_COL : RED_BG_COL); + prayComponent.setBackgroundColor(client.isPrayerActive(nextPrayer) ? medCol : badCol); } else { - prayComponent.setBackgroundColor(client.isPrayerActive(nextPrayer) ? GRN_BG_COL : RED_BG_COL); + prayComponent.setBackgroundColor(client.isPrayerActive(nextPrayer) ? safeCol : badCol); } prayComponent.setImage(hydra.getNextAttack().getImage(spriteManager)); prayComponent.setText(" " + nextSwitch); prayComponent.setColor(Color.white); prayComponent.setPreferredSize(new Dimension(40, 40)); - panelComponent.getChildren().add(prayComponent); - panelComponent.setPreferredSize(new Dimension(40, 0)); - panelComponent.setBorder(new Rectangle(0, 0, 0, 0)); - return panelComponent.render(graphics2D); + panelComponent.getChildren().add(prayComponent); + } + + boolean onGameTick() + { + return --stunTicks <= 0; + } + + private BufferedImage getStunImg() + { + if (stunImg == null) + { + stunImg = createStunImage(client); + } + + return stunImg; + } + + private static BufferedImage createStunImage(Client client) + { + final Sprite root = getSprite(client, SpriteID.BIG_ASS_GREY_ENTANGLE); + final Sprite mark = getSprite(client, SpriteID.TRADE_EXCLAMATION_MARK_ITEM_REMOVAL_WARNING); + + if (mark == null || root == null) + { + return null; + } + + final Sprite sprite = ImageUtil.mergeSprites(client, ImageUtil.resizeSprite(client, root, IMGSIZE, IMGSIZE), mark); + + return sprite.toBufferedImage(); + } + + private static Sprite getSprite(Client client, int id) + { + final IndexDataBase spriteDb = client.getIndexSprites(); + if (spriteDb == null) + { + return null; + } + + final Sprite[] sprites = client.getSprites(spriteDb, id, 0); + if (sprites == null) + { + return null; + } + + return sprites[0]; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraPlugin.java index b0ce57794b..dd3c30ac5b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraPlugin.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.alchemicalhydra; +import com.google.inject.Provides; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -44,9 +45,12 @@ import net.runelite.api.Projectile; import net.runelite.api.coords.LocalPoint; import net.runelite.api.events.AnimationChanged; import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; import net.runelite.api.events.NpcSpawned; import net.runelite.api.events.ProjectileMoved; +import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; @@ -71,6 +75,15 @@ public class HydraPlugin extends Plugin @Getter(AccessLevel.PACKAGE) private Hydra hydra; + @Getter(AccessLevel.PACKAGE) + private boolean counting; + + @Getter(AccessLevel.PACKAGE) + private boolean fountain; + + @Getter(AccessLevel.PACKAGE) + private boolean stun; + private boolean inHydraInstance; private int lastAttackTick; @@ -78,26 +91,40 @@ public class HydraPlugin extends Plugin 5279, 5280, 5535, 5536 }; + private static final int STUN_LENGTH = 7; @Inject private Client client; @Inject - private OverlayManager overlayManager; + private EventBus eventBus; + + @Inject + private HydraConfig config; @Inject private HydraOverlay overlay; @Inject - private HydraSceneOverlay poisonOverlay; + private HydraSceneOverlay sceneOverlay; @Inject - private EventBus eventBus; + private OverlayManager overlayManager; + + @Provides + HydraConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(HydraConfig.class); + } @Override protected void startUp() { + initConfig(); + + eventBus.subscribe(ConfigChanged.class, this, this::onConfigChanged); eventBus.subscribe(GameStateChanged.class, this, this::onGameStateChanged); + inHydraInstance = checkArea(); lastAttackTick = -1; poisonProjectiles.clear(); @@ -117,6 +144,20 @@ public class HydraPlugin extends Plugin lastAttackTick = -1; } + private void initConfig() + { + this.counting = config.counting(); + this.fountain = config.fountain(); + this.stun = config.stun(); + this.overlay.setSafeCol(config.safeCol()); + this.overlay.setMedCol(config.medCol()); + this.overlay.setBadCol(config.badCol()); + this.sceneOverlay.setPoisonBorder(config.poisonBorderCol()); + this.sceneOverlay.setPoisonFill(config.poisonCol()); + this.sceneOverlay.setBadFountain(config.fountainColA()); + this.sceneOverlay.setGoodFountain(config.fountainColB()); + } + private void addFightSubscriptions() { eventBus.subscribe(AnimationChanged.class, "fight", this::onAnimationChanged); @@ -124,6 +165,48 @@ public class HydraPlugin extends Plugin eventBus.subscribe(ChatMessage.class, "fight", this::onChatMessage); } + private void onConfigChanged(ConfigChanged event) + { + if (!event.getGroup().equals("betterHydra")) + { + return; + } + + switch (event.getKey()) + { + case "counting": + this.counting = config.counting(); + break; + case "fountain": + this.fountain = config.fountain(); + break; + case "stun": + this.stun = config.stun(); + break; + case "safeCol": + overlay.setSafeCol(config.safeCol()); + return; + case "medCol": + overlay.setMedCol(config.medCol()); + return; + case "badCol": + overlay.setBadCol(config.badCol()); + return; + case "poisonBorderCol": + sceneOverlay.setPoisonBorder(config.poisonBorderCol()); + break; + case "poisonCol": + sceneOverlay.setPoisonFill(config.poisonCol()); + break; + case "fountainColA": + sceneOverlay.setBadFountain(config.fountainColA()); + break; + case "fountainColB": + sceneOverlay.setGoodFountain(config.fountainColB()); + break; + } + } + private void onGameStateChanged(GameStateChanged state) { if (state.getGameState() != GameState.LOGGED_IN) @@ -270,12 +353,27 @@ public class HydraPlugin extends Plugin private void onChatMessage(ChatMessage event) { - if (!event.getMessage().equals("The chemicals neutralise the Alchemical Hydra's defences!")) + if (event.getMessage().equals("The chemicals neutralise the Alchemical Hydra's defences!")) { - return; + hydra.setWeakened(true); } + else if (event.getMessage().equals("The Alchemical Hydra temporarily stuns you.")) + { + if (isStun()) + { + overlay.setStunTicks(STUN_LENGTH); + eventBus.subscribe(GameTick.class, "hydraStun", this::onGameTick); + } + } + } - hydra.setWeakened(true); + private void onGameTick(GameTick tick) + { + if (overlay.onGameTick()) + { + // unregister self when 7 ticks have passed + eventBus.unregister("hydraStun"); + } } private boolean checkArea() @@ -285,13 +383,20 @@ public class HydraPlugin extends Plugin private void addOverlays() { - overlayManager.add(overlay); - overlayManager.add(poisonOverlay); + if (counting || stun) + { + overlayManager.add(overlay); + } + + if (counting || fountain) + { + overlayManager.add(sceneOverlay); + } } private void removeOverlays() { overlayManager.remove(overlay); - overlayManager.remove(poisonOverlay); + overlayManager.remove(sceneOverlay); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraSceneOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraSceneOverlay.java index a38fa4ecfc..257bf9f6b6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraSceneOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/alchemicalhydra/HydraSceneOverlay.java @@ -34,6 +34,8 @@ import java.util.Collection; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; +import lombok.AccessLevel; +import lombok.Setter; import net.runelite.api.Client; import static net.runelite.api.Perspective.getCanvasTileAreaPoly; import net.runelite.api.Projectile; @@ -47,9 +49,17 @@ import net.runelite.client.ui.overlay.OverlayPosition; @Singleton class HydraSceneOverlay extends Overlay { - private static final Color GREEN = new Color(0, 255, 0, 100); - private static final Color RED = new Color(255, 0, 0, 100); - private static final Color REDFILL = new Color(255, 0, 0, 50); + @Setter(AccessLevel.PACKAGE) + private Color poisonBorder; + + @Setter(AccessLevel.PACKAGE) + private Color poisonFill; + + @Setter(AccessLevel.PACKAGE) + private Color goodFountain; + + @Setter(AccessLevel.PACKAGE) + private Color badFountain; private final HydraPlugin plugin; private final Client client; @@ -69,12 +79,12 @@ class HydraSceneOverlay extends Overlay Hydra hydra = plugin.getHydra(); final Map poisonProjectiles = plugin.getPoisonProjectiles(); - if (!poisonProjectiles.isEmpty()) + if (plugin.isCounting() && !poisonProjectiles.isEmpty()) { drawPoisonArea(graphics, poisonProjectiles); } - if (hydra.getPhase().getFountain() != null) + if (plugin.isFountain() && hydra.getPhase().getFountain() != null) { drawFountain(graphics, hydra); } @@ -103,9 +113,9 @@ class HydraSceneOverlay extends Overlay } graphics.setPaintMode(); - graphics.setColor(RED); + graphics.setColor(poisonBorder); graphics.draw(poisonTiles); - graphics.setColor(REDFILL); + graphics.setColor(poisonFill); graphics.fill(poisonTiles); } @@ -141,11 +151,11 @@ class HydraSceneOverlay extends Overlay if (hydra.getNpc().getWorldArea().intersectsWith(new WorldArea(wp, 1, 1))) // coords { // WHICH FUCKING RETARD DID X, Y, dX, dY, Z???? IT'S XYZdXdY REEEEEEEEEE - color = GREEN; + color = goodFountain; } else { - color = RED; + color = badFountain; } graphics.setColor(color); diff --git a/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java b/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java index 62561f6cec..a4de492f3d 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java @@ -1,582 +1,712 @@ -/* - * Copyright (c) 2018, Jordan Atwood - * 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.util; - -import com.google.common.primitives.Ints; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.geom.AffineTransform; -import java.awt.image.AffineTransformOp; -import java.awt.image.BufferedImage; -import java.awt.image.DirectColorModel; -import java.awt.image.PixelGrabber; -import java.awt.image.RescaleOp; -import java.awt.image.WritableRaster; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Predicate; -import javax.imageio.ImageIO; -import javax.swing.GrayFilter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.IndexedSprite; -import net.runelite.api.Sprite; - -/** - * Various Image/BufferedImage utilities. - */ -@Slf4j -public class ImageUtil -{ - static - { - ImageIO.setUseCache(false); - } - - /** - * Creates a {@link BufferedImage} from an {@link Image}. - * - * @param image An Image to be converted to a BufferedImage. - * @return A BufferedImage instance of the same given image. - */ - public static BufferedImage bufferedImageFromImage(final Image image) - { - if (image instanceof BufferedImage) - { - return (BufferedImage) image; - } - - final BufferedImage out = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); - final Graphics2D g2d = out.createGraphics(); - g2d.drawImage(image, 0, 0, null); - g2d.dispose(); - return out; - } - - /** - * Offsets an image in the grayscale (darkens/brightens) by a given offset. - * - * @param image The image to be darkened or brightened. - * @param offset A signed 8-bit integer value to brighten or darken the image with. - * Values above 0 will brighten, and values below 0 will darken. - * @return The given image with its brightness adjusted by the given offset. - */ - public static BufferedImage grayscaleOffset(final BufferedImage image, final int offset) - { - final float offsetFloat = (float) offset; - final int numComponents = image.getColorModel().getNumComponents(); - final float[] scales = new float[numComponents]; - final float[] offsets = new float[numComponents]; - - Arrays.fill(scales, 1f); - for (int i = 0; i < numComponents; i++) - { - offsets[i] = offsetFloat; - } - // Set alpha to not offset - offsets[numComponents - 1] = 0f; - - return offset(image, scales, offsets); - } - - /** - * Offsets an image in the grayscale (darkens/brightens) by a given percentage. - * - * @param image The image to be darkened or brightened. - * @param percentage The ratio to darken or brighten the given image. - * Values above 1 will brighten, and values below 1 will darken. - * @return The given image with its brightness scaled by the given percentage. - */ - public static BufferedImage grayscaleOffset(final BufferedImage image, final float percentage) - { - final int numComponents = image.getColorModel().getNumComponents(); - final float[] scales = new float[numComponents]; - final float[] offsets = new float[numComponents]; - - Arrays.fill(offsets, 0f); - for (int i = 0; i < numComponents; i++) - { - scales[i] = percentage; - } - // Set alpha to not scale - scales[numComponents - 1] = 1f; - - return offset(image, scales, offsets); - } - - /** - * Offsets an image's alpha component by a given offset. - * - * @param image The image to be made more or less transparent. - * @param offset A signed 8-bit integer value to modify the image's alpha component with. - * Values above 0 will increase transparency, and values below 0 will decrease - * transparency. - * @return The given image with its alpha component adjusted by the given offset. - */ - public static BufferedImage alphaOffset(final BufferedImage image, final int offset) - { - final float offsetFloat = (float) offset; - final int numComponents = image.getColorModel().getNumComponents(); - final float[] scales = new float[numComponents]; - final float[] offsets = new float[numComponents]; - - Arrays.fill(scales, 1f); - Arrays.fill(offsets, 0f); - offsets[numComponents - 1] = offsetFloat; - return offset(image, scales, offsets); - } - - /** - * Offsets an image's alpha component by a given percentage. - * - * @param image The image to be made more or less transparent. - * @param percentage The ratio to modify the image's alpha component with. - * Values above 1 will increase transparency, and values below 1 will decrease - * transparency. - * @return The given image with its alpha component scaled by the given percentage. - */ - public static BufferedImage alphaOffset(final BufferedImage image, final float percentage) - { - final int numComponents = image.getColorModel().getNumComponents(); - final float[] scales = new float[numComponents]; - final float[] offsets = new float[numComponents]; - - Arrays.fill(scales, 1f); - Arrays.fill(offsets, 0f); - scales[numComponents - 1] = percentage; - return offset(image, scales, offsets); - } - - /** - * Creates a grayscale image from the given image. - * - * @param image The source image to be converted. - * @return A copy of the given imnage, with colors converted to grayscale. - */ - public static BufferedImage grayscaleImage(final BufferedImage image) - { - final Image grayImage = GrayFilter.createDisabledImage(image); - return ImageUtil.bufferedImageFromImage(grayImage); - } - - /** - * Re-size a BufferedImage to the given dimensions. - * - * @param image the BufferedImage. - * @param newWidth The width to set the BufferedImage to. - * @param newHeight The height to set the BufferedImage to. - * @return The BufferedImage with the specified dimensions - */ - public static BufferedImage resizeImage(final BufferedImage image, final int newWidth, final int newHeight) - { - final Image resized = image.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); - return ImageUtil.bufferedImageFromImage(resized); - } - - /** - * Re-size a BufferedImage's canvas to the given dimensions. - * - * @param image The image whose canvas should be re-sized. - * @param newWidth The width to set the BufferedImage to. - * @param newHeight The height to set the BufferedImage to. - * @return The BufferedImage centered within canvas of given dimensions. - */ - public static BufferedImage resizeCanvas(final BufferedImage image, final int newWidth, final int newHeight) - { - final BufferedImage dimg = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); - final int centeredX = newWidth / 2 - image.getWidth() / 2; - final int centeredY = newHeight / 2 - image.getHeight() / 2; - - final Graphics2D g2d = dimg.createGraphics(); - g2d.drawImage(image, centeredX, centeredY, null); - g2d.dispose(); - return dimg; - } - - /** - * Rotates an image around its center by a given number of radians. - * - * @param image The image to be rotated. - * @param theta The number of radians to rotate the image. - * @return The given image, rotated by the given theta. - */ - public static BufferedImage rotateImage(final BufferedImage image, final double theta) - { - AffineTransform transform = new AffineTransform(); - transform.rotate(theta, image.getWidth() / 2.0, image.getHeight() / 2.0); - AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); - return transformOp.filter(image, null); - } - - /** - * Flips an image horizontally and/or vertically. - * - * @param image The image to be flipped. - * @param horizontal Whether the image should be flipped horizontally. - * @param vertical Whether the image should be flipped vertically. - * @return The given image, flipped horizontally and/or vertically. - */ - public static BufferedImage flipImage(final BufferedImage image, final boolean horizontal, final boolean vertical) - { - int x = 0; - int y = 0; - int w = image.getWidth(); - int h = image.getHeight(); - - final BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - final Graphics2D g2d = out.createGraphics(); - - if (horizontal) - { - x = w; - w *= -1; - } - - if (vertical) - { - y = h; - h *= -1; - } - - g2d.drawImage(image, x, y, w, h, null); - g2d.dispose(); - - return out; - } - - /** - * Outlines non-transparent pixels of a BufferedImage with the given color. - * - * @param image The image to be outlined. - * @param color The color to use for the outline. - * @return The BufferedImage with its edges outlined with the given color. - */ - public static BufferedImage outlineImage(final BufferedImage image, final Color color) - { - return outlineImage(image, color, ColorUtil::isNotFullyTransparent, false); - } - - /** - * Outlines pixels of a BufferedImage with the given color, using a given predicate to colorize - * the given image for outlining. - * - * @param image The image to be outlined. - * @param color The color to use for the outline. - * @param fillCondition The predicate to be consumed by {@link #fillImage(BufferedImage, Color, Predicate) fillImage(BufferedImage, Color, Predicate)} - * @return The BufferedImage with its edges outlined with the given color. - */ - public static BufferedImage outlineImage(final BufferedImage image, final Color color, final Predicate fillCondition) - { - return outlineImage(image, color, fillCondition, false); - } - - /** - * Outlines non-transparent pixels of a BufferedImage with the given color. Optionally outlines - * corners in addition to edges. - * - * @param image The image to be outlined. - * @param color The color to use for the outline. - * @param outlineCorners Whether to draw an outline around corners, or only around edges. - * @return The BufferedImage with its edges--and optionally, corners--outlined - * with the given color. - */ - public static BufferedImage outlineImage(final BufferedImage image, final Color color, final Boolean outlineCorners) - { - return outlineImage(image, color, ColorUtil::isNotFullyTransparent, outlineCorners); - } - - /** - * Outlines pixels of a BufferedImage with the given color, using a given predicate to colorize - * the given image for outlining. Optionally outlines corners in addition to edges. - * - * @param image The image to be outlined. - * @param color The color to use for the outline. - * @param fillCondition The predicate to be consumed by {@link #fillImage(BufferedImage, Color, Predicate) fillImage(BufferedImage, Color, Predicate)} - * @param outlineCorners Whether to draw an outline around corners, or only around edges. - * @return The BufferedImage with its edges--and optionally, corners--outlined - * with the given color. - */ - public static BufferedImage outlineImage(final BufferedImage image, final Color color, final Predicate fillCondition, final Boolean outlineCorners) - { - final BufferedImage filledImage = fillImage(image, color, fillCondition); - final BufferedImage outlinedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); - - final Graphics2D g2d = outlinedImage.createGraphics(); - for (int x = -1; x <= 1; x++) - { - for (int y = -1; y <= 1; y++) - { - if ((x == 0 && y == 0) - || (!outlineCorners && Math.abs(x) + Math.abs(y) != 1)) - { - continue; - } - - g2d.drawImage(filledImage, x, y, null); - } - } - g2d.drawImage(image, 0, 0, null); - g2d.dispose(); - - return outlinedImage; - } - - /** - * Reads an image resource from a given path relative to a given class. - * This method is primarily shorthand for the synchronization and error handling required for - * loading image resources from classes. - * - * @param c The class to be referenced for resource path. - * @param path The path, relative to the given class. - * @return A {@link BufferedImage} of the loaded image resource from the given path. - */ - public static BufferedImage getResourceStreamFromClass(final Class c, final String path) - { - try - { - synchronized (ImageIO.class) - { - return ImageIO.read(c.getResourceAsStream(path)); - } - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - /** - * Fills all non-transparent pixels of the given image with the given color. - * - * @param image The image which should have its non-transparent pixels filled. - * @param color The color with which to fill pixels. - * @return The given image with all non-transparent pixels set to the given color. - */ - public static BufferedImage fillImage(final BufferedImage image, final Color color) - { - return fillImage(image, color, ColorUtil::isNotFullyTransparent); - } - - /** - * Fills pixels of the given image with the given color based on a given fill condition - * predicate. - * - * @param image The image which should have its non-transparent pixels filled. - * @param color The color with which to fill pixels. - * @param fillCondition The condition on which to fill pixels with the given color. - * @return The given image with all pixels fulfilling the fill condition predicate - * set to the given color. - */ - static BufferedImage fillImage(final BufferedImage image, final Color color, final Predicate fillCondition) - { - final BufferedImage filledImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); - for (int x = 0; x < filledImage.getWidth(); x++) - { - for (int y = 0; y < filledImage.getHeight(); y++) - { - final Color pixelColor = new Color(image.getRGB(x, y), true); - if (!fillCondition.test(pixelColor)) - { - continue; - } - - filledImage.setRGB(x, y, color.getRGB()); - } - } - return filledImage; - } - - /** - * Recolors pixels of the given image with the given color based on a given recolor condition - * predicate. - * - * @param image The image which should have its non-transparent pixels recolored. - * @param color The color with which to recolor pixels. - * @param recolorCondition The condition on which to recolor pixels with the given color. - * @return The given image with all pixels fulfilling the recolor condition predicate - * set to the given color. - */ - public static BufferedImage recolorImage(final BufferedImage image, final Color color, final Predicate recolorCondition) - { - final BufferedImage recoloredImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); - for (int x = 0; x < recoloredImage.getWidth(); x++) - { - for (int y = 0; y < recoloredImage.getHeight(); y++) - { - final Color pixelColor = new Color(image.getRGB(x, y), true); - if (!recolorCondition.test(pixelColor)) - { - recoloredImage.setRGB(x, y, image.getRGB(x, y)); - continue; - } - - recoloredImage.setRGB(x, y, color.getRGB()); - } - } - return recoloredImage; - } - - public static BufferedImage recolorImage(BufferedImage image, final Color color) - { - int width = image.getWidth(); - int height = image.getHeight(); - WritableRaster raster = image.getRaster(); - - for (int xx = 0; xx < width; xx++) - { - for (int yy = 0; yy < height; yy++) - { - int[] pixels = raster.getPixel(xx, yy, (int[]) null); - pixels[0] = color.getRed(); - pixels[1] = color.getGreen(); - pixels[2] = color.getBlue(); - raster.setPixel(xx, yy, pixels); - } - } - return image; - } - - /** - * Performs a rescale operation on the image's color components. - * - * @param image The image to be adjusted. - * @param scales An array of scale operations to be performed on the image's color components. - * @param offsets An array of offset operations to be performed on the image's color components. - * @return The modified image after applying the given adjustments. - */ - private static BufferedImage offset(final BufferedImage image, final float[] scales, final float[] offsets) - { - return new RescaleOp(scales, offsets, null).filter(image, null); - } - - - /** - * Converts the buffered image into a sprite image and returns it - * - * @param image The image to be converted - * @param client Current client instance - * @return The buffered image as a sprite image - */ - public static Sprite getImageSprite(BufferedImage image, Client client) - { - int[] pixels = new int[image.getWidth() * image.getHeight()]; - - try - { - PixelGrabber g = new PixelGrabber(image, 0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); - g.setColorModel(new DirectColorModel(32, 0xff0000, 0xff00, 0xff, 0xff000000)); - g.grabPixels(); - - // Make any fully transparent pixels fully black, because the sprite draw routines - // check for == 0, not actual transparency - for (int i = 0; i < pixels.length; i++) - { - if ((pixels[i] & 0xFF000000) == 0) - { - pixels[i] = 0; - } - } - } - catch (InterruptedException ex) - { - log.debug("PixelGrabber was interrupted: ", ex); - } - - return client.createSprite(pixels, image.getWidth(), image.getHeight()); - } - - /** - * Converts an image into an {@code IndexedSprite} instance. - *

- * The passed in image can only have at max 255 different colors. - * - * @param image The image to be converted - * @param client Current client instance - * @return The image as an {@code IndexedSprite} - */ - public static IndexedSprite getImageIndexedSprite(BufferedImage image, Client client) - { - final byte[] pixels = new byte[image.getWidth() * image.getHeight()]; - final List palette = new ArrayList<>(); - /* - When drawing the indexed sprite, palette idx 0 is seen as fully transparent, - so pad the palette out so that our colors start at idx 1. - */ - palette.add(0); - - final int[] sourcePixels = image.getRGB(0, 0, - image.getWidth(), image.getHeight(), - null, 0, image.getWidth()); - - /* - Build a color palette and assign the pixels to positions in the palette. - */ - for (int j = 0; j < sourcePixels.length; j++) - { - final int argb = sourcePixels[j]; - final int a = (argb >> 24) & 0xFF; - final int rgb = argb & 0xFF_FF_FF; - - // Default to not drawing the pixel. - int paletteIdx = 0; - - // If the pixel is fully opaque, draw it. - if (a == 0xFF) - { - paletteIdx = palette.indexOf(rgb); - - if (paletteIdx == -1) - { - paletteIdx = palette.size(); - palette.add(rgb); - } - } - - pixels[j] = (byte) paletteIdx; - } - - if (palette.size() > 256) - { - throw new RuntimeException("Passed in image had " + (palette.size() - 1) - + " different colors, exceeding the max of 255."); - } - - final IndexedSprite sprite = client.createIndexedSprite(); - - sprite.setPixels(pixels); - sprite.setPalette(Ints.toArray(palette)); - sprite.setWidth(image.getWidth()); - sprite.setHeight(image.getHeight()); - sprite.setOriginalWidth(image.getWidth()); - sprite.setOriginalHeight(image.getHeight()); - sprite.setOffsetX(0); - sprite.setOffsetY(0); - - return sprite; - } -} +/* + * Copyright (c) 2018, Jordan Atwood + * 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.util; + +import com.google.common.primitives.Ints; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.awt.image.DirectColorModel; +import java.awt.image.PixelGrabber; +import java.awt.image.RescaleOp; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import javax.imageio.ImageIO; +import javax.swing.GrayFilter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.IndexedSprite; +import net.runelite.api.Sprite; + +/** + * Various Image/BufferedImage utilities. + */ +@Slf4j +public class ImageUtil +{ + static + { + ImageIO.setUseCache(false); + } + + /** + * Creates a {@link BufferedImage} from an {@link Image}. + * + * @param image An Image to be converted to a BufferedImage. + * @return A BufferedImage instance of the same given image. + */ + public static BufferedImage bufferedImageFromImage(final Image image) + { + if (image instanceof BufferedImage) + { + return (BufferedImage) image; + } + + final BufferedImage out = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); + final Graphics2D g2d = out.createGraphics(); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + return out; + } + + /** + * Offsets an image in the grayscale (darkens/brightens) by a given offset. + * + * @param image The image to be darkened or brightened. + * @param offset A signed 8-bit integer value to brighten or darken the image with. + * Values above 0 will brighten, and values below 0 will darken. + * @return The given image with its brightness adjusted by the given offset. + */ + public static BufferedImage grayscaleOffset(final BufferedImage image, final int offset) + { + final float offsetFloat = (float) offset; + final int numComponents = image.getColorModel().getNumComponents(); + final float[] scales = new float[numComponents]; + final float[] offsets = new float[numComponents]; + + Arrays.fill(scales, 1f); + for (int i = 0; i < numComponents; i++) + { + offsets[i] = offsetFloat; + } + // Set alpha to not offset + offsets[numComponents - 1] = 0f; + + return offset(image, scales, offsets); + } + + /** + * Offsets an image in the grayscale (darkens/brightens) by a given percentage. + * + * @param image The image to be darkened or brightened. + * @param percentage The ratio to darken or brighten the given image. + * Values above 1 will brighten, and values below 1 will darken. + * @return The given image with its brightness scaled by the given percentage. + */ + public static BufferedImage grayscaleOffset(final BufferedImage image, final float percentage) + { + final int numComponents = image.getColorModel().getNumComponents(); + final float[] scales = new float[numComponents]; + final float[] offsets = new float[numComponents]; + + Arrays.fill(offsets, 0f); + for (int i = 0; i < numComponents; i++) + { + scales[i] = percentage; + } + // Set alpha to not scale + scales[numComponents - 1] = 1f; + + return offset(image, scales, offsets); + } + + /** + * Offsets an image's alpha component by a given offset. + * + * @param image The image to be made more or less transparent. + * @param offset A signed 8-bit integer value to modify the image's alpha component with. + * Values above 0 will increase transparency, and values below 0 will decrease + * transparency. + * @return The given image with its alpha component adjusted by the given offset. + */ + public static BufferedImage alphaOffset(final BufferedImage image, final int offset) + { + final float offsetFloat = (float) offset; + final int numComponents = image.getColorModel().getNumComponents(); + final float[] scales = new float[numComponents]; + final float[] offsets = new float[numComponents]; + + Arrays.fill(scales, 1f); + Arrays.fill(offsets, 0f); + offsets[numComponents - 1] = offsetFloat; + return offset(image, scales, offsets); + } + + /** + * Offsets an image's alpha component by a given percentage. + * + * @param image The image to be made more or less transparent. + * @param percentage The ratio to modify the image's alpha component with. + * Values above 1 will increase transparency, and values below 1 will decrease + * transparency. + * @return The given image with its alpha component scaled by the given percentage. + */ + public static BufferedImage alphaOffset(final BufferedImage image, final float percentage) + { + final int numComponents = image.getColorModel().getNumComponents(); + final float[] scales = new float[numComponents]; + final float[] offsets = new float[numComponents]; + + Arrays.fill(scales, 1f); + Arrays.fill(offsets, 0f); + scales[numComponents - 1] = percentage; + return offset(image, scales, offsets); + } + + /** + * Creates a grayscale image from the given image. + * + * @param image The source image to be converted. + * @return A copy of the given imnage, with colors converted to grayscale. + */ + public static BufferedImage grayscaleImage(final BufferedImage image) + { + final Image grayImage = GrayFilter.createDisabledImage(image); + return ImageUtil.bufferedImageFromImage(grayImage); + } + + /** + * Re-size a BufferedImage to the given dimensions. + * + * @param image the BufferedImage. + * @param newWidth The width to set the BufferedImage to. + * @param newHeight The height to set the BufferedImage to. + * @return The BufferedImage with the specified dimensions + */ + public static BufferedImage resizeImage(final BufferedImage image, final int newWidth, final int newHeight) + { + final Image resized = image.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); + return ImageUtil.bufferedImageFromImage(resized); + } + + /** + * Re-size a BufferedImage's canvas to the given dimensions. + * + * @param image The image whose canvas should be re-sized. + * @param newWidth The width to set the BufferedImage to. + * @param newHeight The height to set the BufferedImage to. + * @return The BufferedImage centered within canvas of given dimensions. + */ + public static BufferedImage resizeCanvas(final BufferedImage image, final int newWidth, final int newHeight) + { + final BufferedImage dimg = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); + final int centeredX = newWidth / 2 - image.getWidth() / 2; + final int centeredY = newHeight / 2 - image.getHeight() / 2; + + final Graphics2D g2d = dimg.createGraphics(); + g2d.drawImage(image, centeredX, centeredY, null); + g2d.dispose(); + return dimg; + } + + /** + * Rotates an image around its center by a given number of radians. + * + * @param image The image to be rotated. + * @param theta The number of radians to rotate the image. + * @return The given image, rotated by the given theta. + */ + public static BufferedImage rotateImage(final BufferedImage image, final double theta) + { + AffineTransform transform = new AffineTransform(); + transform.rotate(theta, image.getWidth() / 2.0, image.getHeight() / 2.0); + AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); + return transformOp.filter(image, null); + } + + /** + * Flips an image horizontally and/or vertically. + * + * @param image The image to be flipped. + * @param horizontal Whether the image should be flipped horizontally. + * @param vertical Whether the image should be flipped vertically. + * @return The given image, flipped horizontally and/or vertically. + */ + public static BufferedImage flipImage(final BufferedImage image, final boolean horizontal, final boolean vertical) + { + int x = 0; + int y = 0; + int w = image.getWidth(); + int h = image.getHeight(); + + final BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + final Graphics2D g2d = out.createGraphics(); + + if (horizontal) + { + x = w; + w *= -1; + } + + if (vertical) + { + y = h; + h *= -1; + } + + g2d.drawImage(image, x, y, w, h, null); + g2d.dispose(); + + return out; + } + + /** + * Outlines non-transparent pixels of a BufferedImage with the given color. + * + * @param image The image to be outlined. + * @param color The color to use for the outline. + * @return The BufferedImage with its edges outlined with the given color. + */ + public static BufferedImage outlineImage(final BufferedImage image, final Color color) + { + return outlineImage(image, color, ColorUtil::isNotFullyTransparent, false); + } + + /** + * Outlines pixels of a BufferedImage with the given color, using a given predicate to colorize + * the given image for outlining. + * + * @param image The image to be outlined. + * @param color The color to use for the outline. + * @param fillCondition The predicate to be consumed by {@link #fillImage(BufferedImage, Color, Predicate) fillImage(BufferedImage, Color, Predicate)} + * @return The BufferedImage with its edges outlined with the given color. + */ + public static BufferedImage outlineImage(final BufferedImage image, final Color color, final Predicate fillCondition) + { + return outlineImage(image, color, fillCondition, false); + } + + /** + * Outlines non-transparent pixels of a BufferedImage with the given color. Optionally outlines + * corners in addition to edges. + * + * @param image The image to be outlined. + * @param color The color to use for the outline. + * @param outlineCorners Whether to draw an outline around corners, or only around edges. + * @return The BufferedImage with its edges--and optionally, corners--outlined + * with the given color. + */ + public static BufferedImage outlineImage(final BufferedImage image, final Color color, final Boolean outlineCorners) + { + return outlineImage(image, color, ColorUtil::isNotFullyTransparent, outlineCorners); + } + + /** + * Outlines pixels of a BufferedImage with the given color, using a given predicate to colorize + * the given image for outlining. Optionally outlines corners in addition to edges. + * + * @param image The image to be outlined. + * @param color The color to use for the outline. + * @param fillCondition The predicate to be consumed by {@link #fillImage(BufferedImage, Color, Predicate) fillImage(BufferedImage, Color, Predicate)} + * @param outlineCorners Whether to draw an outline around corners, or only around edges. + * @return The BufferedImage with its edges--and optionally, corners--outlined + * with the given color. + */ + public static BufferedImage outlineImage(final BufferedImage image, final Color color, final Predicate fillCondition, final Boolean outlineCorners) + { + final BufferedImage filledImage = fillImage(image, color, fillCondition); + final BufferedImage outlinedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + + final Graphics2D g2d = outlinedImage.createGraphics(); + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + if ((x == 0 && y == 0) + || (!outlineCorners && Math.abs(x) + Math.abs(y) != 1)) + { + continue; + } + + g2d.drawImage(filledImage, x, y, null); + } + } + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + + return outlinedImage; + } + + /** + * Reads an image resource from a given path relative to a given class. + * This method is primarily shorthand for the synchronization and error handling required for + * loading image resources from classes. + * + * @param c The class to be referenced for resource path. + * @param path The path, relative to the given class. + * @return A {@link BufferedImage} of the loaded image resource from the given path. + */ + public static BufferedImage getResourceStreamFromClass(final Class c, final String path) + { + try + { + synchronized (ImageIO.class) + { + return ImageIO.read(c.getResourceAsStream(path)); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + /** + * Fills all non-transparent pixels of the given image with the given color. + * + * @param image The image which should have its non-transparent pixels filled. + * @param color The color with which to fill pixels. + * @return The given image with all non-transparent pixels set to the given color. + */ + public static BufferedImage fillImage(final BufferedImage image, final Color color) + { + return fillImage(image, color, ColorUtil::isNotFullyTransparent); + } + + /** + * Fills pixels of the given image with the given color based on a given fill condition + * predicate. + * + * @param image The image which should have its non-transparent pixels filled. + * @param color The color with which to fill pixels. + * @param fillCondition The condition on which to fill pixels with the given color. + * @return The given image with all pixels fulfilling the fill condition predicate + * set to the given color. + */ + static BufferedImage fillImage(final BufferedImage image, final Color color, final Predicate fillCondition) + { + final BufferedImage filledImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < filledImage.getWidth(); x++) + { + for (int y = 0; y < filledImage.getHeight(); y++) + { + final Color pixelColor = new Color(image.getRGB(x, y), true); + if (!fillCondition.test(pixelColor)) + { + continue; + } + + filledImage.setRGB(x, y, color.getRGB()); + } + } + return filledImage; + } + + /** + * Recolors pixels of the given image with the given color based on a given recolor condition + * predicate. + * + * @param image The image which should have its non-transparent pixels recolored. + * @param color The color with which to recolor pixels. + * @param recolorCondition The condition on which to recolor pixels with the given color. + * @return The given image with all pixels fulfilling the recolor condition predicate + * set to the given color. + */ + public static BufferedImage recolorImage(final BufferedImage image, final Color color, final Predicate recolorCondition) + { + final BufferedImage recoloredImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < recoloredImage.getWidth(); x++) + { + for (int y = 0; y < recoloredImage.getHeight(); y++) + { + final Color pixelColor = new Color(image.getRGB(x, y), true); + if (!recolorCondition.test(pixelColor)) + { + recoloredImage.setRGB(x, y, image.getRGB(x, y)); + continue; + } + + recoloredImage.setRGB(x, y, color.getRGB()); + } + } + return recoloredImage; + } + + public static BufferedImage recolorImage(BufferedImage image, final Color color) + { + int width = image.getWidth(); + int height = image.getHeight(); + WritableRaster raster = image.getRaster(); + + for (int xx = 0; xx < width; xx++) + { + for (int yy = 0; yy < height; yy++) + { + int[] pixels = raster.getPixel(xx, yy, (int[]) null); + pixels[0] = color.getRed(); + pixels[1] = color.getGreen(); + pixels[2] = color.getBlue(); + raster.setPixel(xx, yy, pixels); + } + } + return image; + } + + /** + * Performs a rescale operation on the image's color components. + * + * @param image The image to be adjusted. + * @param scales An array of scale operations to be performed on the image's color components. + * @param offsets An array of offset operations to be performed on the image's color components. + * @return The modified image after applying the given adjustments. + */ + private static BufferedImage offset(final BufferedImage image, final float[] scales, final float[] offsets) + { + return new RescaleOp(scales, offsets, null).filter(image, null); + } + + + /** + * Converts the buffered image into a sprite image and returns it + * + * @param image The image to be converted + * @param client Current client instance + * @return The buffered image as a sprite image + */ + public static Sprite getImageSprite(BufferedImage image, Client client) + { + int[] pixels = new int[image.getWidth() * image.getHeight()]; + + try + { + PixelGrabber g = new PixelGrabber(image, 0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); + g.setColorModel(new DirectColorModel(32, 0xff0000, 0xff00, 0xff, 0xff000000)); + g.grabPixels(); + + // Make any fully transparent pixels fully black, because the sprite draw routines + // check for == 0, not actual transparency + for (int i = 0; i < pixels.length; i++) + { + if ((pixels[i] & 0xFF000000) == 0) + { + pixels[i] = 0; + } + } + } + catch (InterruptedException ex) + { + log.debug("PixelGrabber was interrupted: ", ex); + } + + return client.createSprite(pixels, image.getWidth(), image.getHeight()); + } + + /** + * Converts an image into an {@code IndexedSprite} instance. + *

+ * The passed in image can only have at max 255 different colors. + * + * @param image The image to be converted + * @param client Current client instance + * @return The image as an {@code IndexedSprite} + */ + public static IndexedSprite getImageIndexedSprite(BufferedImage image, Client client) + { + final byte[] pixels = new byte[image.getWidth() * image.getHeight()]; + final List palette = new ArrayList<>(); + /* + When drawing the indexed sprite, palette idx 0 is seen as fully transparent, + so pad the palette out so that our colors start at idx 1. + */ + palette.add(0); + + final int[] sourcePixels = image.getRGB(0, 0, + image.getWidth(), image.getHeight(), + null, 0, image.getWidth()); + + /* + Build a color palette and assign the pixels to positions in the palette. + */ + for (int j = 0; j < sourcePixels.length; j++) + { + final int argb = sourcePixels[j]; + final int a = (argb >> 24) & 0xFF; + final int rgb = argb & 0xFF_FF_FF; + + // Default to not drawing the pixel. + int paletteIdx = 0; + + // If the pixel is fully opaque, draw it. + if (a == 0xFF) + { + paletteIdx = palette.indexOf(rgb); + + if (paletteIdx == -1) + { + paletteIdx = palette.size(); + palette.add(rgb); + } + } + + pixels[j] = (byte) paletteIdx; + } + + if (palette.size() > 256) + { + throw new RuntimeException("Passed in image had " + (palette.size() - 1) + + " different colors, exceeding the max of 255."); + } + + final IndexedSprite sprite = client.createIndexedSprite(); + + sprite.setPixels(pixels); + sprite.setPalette(Ints.toArray(palette)); + sprite.setWidth(image.getWidth()); + sprite.setHeight(image.getHeight()); + sprite.setOriginalWidth(image.getWidth()); + sprite.setOriginalHeight(image.getHeight()); + sprite.setOffsetX(0); + sprite.setOffsetY(0); + + return sprite; + } + + /** + * Resize Sprite sprite to given width (newW) and height (newH) + */ + public static Sprite resizeSprite(final Client client, final Sprite sprite, int newW, int newH) + { + assert newW > 0 && newH > 0; + + final int oldW = sprite.getWidth(); + final int oldH = sprite.getHeight(); + + if (oldW == newW && oldH == newH) + { + return sprite; + } + + final int[] canvas = new int[newW * newH]; + final int[] pixels = sprite.getPixels(); + + final Sprite result = client.createSprite(canvas, newW, newH); + + int pixelX = 0; + int pixelY = 0; + + final int oldMaxW = sprite.getMaxWidth(); + final int oldMaxH = sprite.getMaxHeight(); + + final int pixelW = (oldMaxW << 16) / newW; + final int pixelH = (oldMaxH << 16) / newH; + + int xOffset = 0; + int yOffset = 0; + + int canvasIdx; + if (sprite.getOffsetX() > 0) + { + canvasIdx = (pixelW + (sprite.getOffsetX() << 16) - 1) / pixelW; + xOffset += canvasIdx; + pixelX += canvasIdx * pixelW - (sprite.getOffsetX() << 16); + } + + if (sprite.getOffsetY() > 0) + { + canvasIdx = (pixelH + (sprite.getOffsetY() << 16) - 1) / pixelH; + yOffset += canvasIdx; + pixelY += canvasIdx * pixelH - (sprite.getOffsetY() << 16); + } + + if (oldW < oldMaxW) + { + newW = (pixelW + ((oldW << 16) - pixelX) - 1) / pixelW; + } + + if (oldH < oldMaxH) + { + newH = (pixelH + ((oldH << 16) - pixelY) - 1) / pixelH; + } + + canvasIdx = xOffset + yOffset * newW; + int canvasOffset = 0; + if (yOffset + newH > newH) + { + newH -= yOffset + newH - newH; + } + + int tmp; + if (yOffset < 0) + { + tmp = -yOffset; + newH -= tmp; + canvasIdx += tmp * newW; + pixelY += pixelH * tmp; + } + + if (newW + xOffset > newW) + { + tmp = newW + xOffset - newW; + newW -= tmp; + canvasOffset += tmp; + } + + if (xOffset < 0) + { + tmp = -xOffset; + newW -= tmp; + canvasIdx += tmp; + pixelX += pixelW * tmp; + canvasOffset += tmp; + } + + client.scaleSprite(canvas, pixels, 0, pixelX, pixelY, canvasIdx, canvasOffset, newW, newH, pixelW, pixelH, oldW); + + return result; + } + + /** + * Draw fg centered on top of bg + */ + public static Sprite mergeSprites(final Client client, final Sprite bg, final Sprite fg) + { + assert fg.getHeight() <= bg.getHeight() && fg.getWidth() <= bg.getWidth() : "Background has to be larger than foreground"; + + final int[] canvas = Arrays.copyOf(bg.getPixels(), bg.getWidth() * bg.getHeight()); + final Sprite result = client.createSprite(canvas, bg.getWidth(), bg.getHeight()); + + final int bgWid = bg.getWidth(); + final int fgHgt = fg.getHeight(); + final int fgWid = fg.getWidth(); + + final int xOffset = (bgWid - fgWid) / 2; + final int yOffset = (bg.getHeight() - fgHgt) / 2; + + final int[] fgPixels = fg.getPixels(); + + for (int y1 = yOffset, y2 = 0; y2 < fgHgt; y1++, y2++) + { + int i1 = y1 * bgWid + xOffset; + int i2 = y2 * fgWid; + + for (int x = 0; x < fgWid; x++, i1++, i2++) + { + if (fgPixels[i2] > 0) + { + canvas[i1] = fgPixels[i2]; + } + } + } + + return result; + } +} diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java index 8e1c1f89bd..0d09db8df5 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java @@ -1086,4 +1086,8 @@ public interface RSClient extends RSGameShell, Client @Import("selectedSpellChildIndex") @Override void setSelectedSpellChildIndex(int index); + + @Import("Sprite_drawScaled") + @Override + void scaleSprite(int[] canvas, int[] pixels, int color, int pixelX, int pixelY, int canvasIdx, int canvasOffset, int newWidth, int newHeight, int pixelWidth, int pixelHeight, int oldWidth); } \ No newline at end of file diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSSprite.java b/runescape-api/src/main/java/net/runelite/rs/api/RSSprite.java index 1f5ee77b46..64503ff099 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSSprite.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSSprite.java @@ -25,14 +25,34 @@ public interface RSSprite extends Sprite void setRaster(); @Import("width") + @Override + int getMaxWidth(); + + @Import("width") + @Override void setMaxWidth(int maxWidth); @Import("height") + @Override + int getMaxHeight(); + + @Import("height") + @Override void setMaxHeight(int maxHeight); @Import("xOffset") + @Override + int getOffsetX(); ; + + @Import("xOffset") + @Override void setOffsetX(int offsetX); @Import("yOffset") + @Override + int getOffsetY(); ; + + @Import("yOffset") + @Override void setOffsetY(int offsetY); }