From 7cbef5edaa7970a583f760ee4135d9778b1b342f Mon Sep 17 00:00:00 2001 From: Max Weber Date: Fri, 15 Oct 2021 17:48:00 -0600 Subject: [PATCH 01/29] cache: unify jagex hsl conversion --- .../net/runelite/cache/MapImageDumper.java | 4 +- .../net/runelite/cache/item/ColorPalette.java | 161 ------------------ .../net/runelite/cache/item/Graphics3D.java | 6 +- .../cache/item/ItemSpriteFactory.java | 3 +- .../net/runelite/cache/models/JagexColor.java | 12 +- .../runelite/cache/models/ObjExporter.java | 2 +- .../runelite/cache/models/JagexColorTest.java | 2 +- 7 files changed, 21 insertions(+), 169 deletions(-) delete mode 100644 cache/src/main/java/net/runelite/cache/item/ColorPalette.java diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index af857e796e..833b4c1fc1 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -47,8 +47,8 @@ import net.runelite.cache.fs.FSFile; import net.runelite.cache.fs.Index; import net.runelite.cache.fs.Storage; import net.runelite.cache.fs.Store; -import net.runelite.cache.item.ColorPalette; import net.runelite.cache.item.RSTextureProvider; +import net.runelite.cache.models.JagexColor; import net.runelite.cache.region.Location; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; @@ -65,7 +65,7 @@ public class MapImageDumper private static final int MAPICON_MAX_HEIGHT = 6; private static final int BLEND = 5; // number of surrounding tiles for ground blending - private static int[] colorPalette = new ColorPalette(0.9d, 0, 512).getColorPalette(); + private static int[] colorPalette = JagexColor.createPalette(JagexColor.BRIGHTNESS_MIN); private static int[][] TILE_SHAPE_2D = new int[][]{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}, {1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1}, {1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1}}; private static int[][] TILE_ROTATION_2D = new int[][]{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, {12, 8, 4, 0, 13, 9, 5, 1, 14, 10, 6, 2, 15, 11, 7, 3}, {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}, {3, 7, 11, 15, 2, 6, 10, 14, 1, 5, 9, 13, 0, 4, 8, 12}}; diff --git a/cache/src/main/java/net/runelite/cache/item/ColorPalette.java b/cache/src/main/java/net/runelite/cache/item/ColorPalette.java deleted file mode 100644 index 6486afe2fb..0000000000 --- a/cache/src/main/java/net/runelite/cache/item/ColorPalette.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2018, Adam - * 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.cache.item; - -import lombok.Getter; - -public class ColorPalette -{ - @Getter - private final int[] colorPalette; - - public ColorPalette(double brightness, int var2, int var3) - { - colorPalette = buildColorPalettee(brightness, var2, var3); - } - - private int[] buildColorPalettee(double brightness, int var2, int var3) - { - int[] colorPalette = new int[65536]; - int var4 = var2 * 128; - - for (int var5 = var2; var5 < var3; ++var5) - { - double var6 = (double) (var5 >> 3) / 64.0D + 0.0078125D; - double var8 = (double) (var5 & 7) / 8.0D + 0.0625D; - - for (int var10 = 0; var10 < 128; ++var10) - { - double var11 = (double) var10 / 128.0D; - double var13 = var11; - double var15 = var11; - double var17 = var11; - if (var8 != 0.0D) - { - double var19; - if (var11 < 0.5D) - { - var19 = var11 * (1.0D + var8); - } - else - { - var19 = var11 + var8 - var11 * var8; - } - - double var21 = 2.0D * var11 - var19; - double var23 = var6 + 0.3333333333333333D; - if (var23 > 1.0D) - { - --var23; - } - - double var27 = var6 - 0.3333333333333333D; - if (var27 < 0.0D) - { - ++var27; - } - - if (6.0D * var23 < 1.0D) - { - var13 = var21 + (var19 - var21) * 6.0D * var23; - } - else if (2.0D * var23 < 1.0D) - { - var13 = var19; - } - else if (3.0D * var23 < 2.0D) - { - var13 = var21 + (var19 - var21) * (0.6666666666666666D - var23) * 6.0D; - } - else - { - var13 = var21; - } - - if (6.0D * var6 < 1.0D) - { - var15 = var21 + (var19 - var21) * 6.0D * var6; - } - else if (2.0D * var6 < 1.0D) - { - var15 = var19; - } - else if (3.0D * var6 < 2.0D) - { - var15 = var21 + (var19 - var21) * (0.6666666666666666D - var6) * 6.0D; - } - else - { - var15 = var21; - } - - if (6.0D * var27 < 1.0D) - { - var17 = var21 + (var19 - var21) * 6.0D * var27; - } - else if (2.0D * var27 < 1.0D) - { - var17 = var19; - } - else if (3.0D * var27 < 2.0D) - { - var17 = var21 + (var19 - var21) * (0.6666666666666666D - var27) * 6.0D; - } - else - { - var17 = var21; - } - } - - int var29 = (int) (var13 * 256.0D); - int var20 = (int) (var15 * 256.0D); - int var30 = (int) (var17 * 256.0D); - int var22 = var30 + (var20 << 8) + (var29 << 16); - var22 = adjustRGB(var22, brightness); - if (var22 == 0) - { - var22 = 1; - } - - colorPalette[var4++] = var22; - } - } - return colorPalette; - } - - private static int adjustRGB(int var0, double var1) - { - double var3 = (double) (var0 >> 16) / 256.0D; - double var5 = (double) (var0 >> 8 & 255) / 256.0D; - double var7 = (double) (var0 & 255) / 256.0D; - var3 = Math.pow(var3, var1); - var5 = Math.pow(var5, var1); - var7 = Math.pow(var7, var1); - int var9 = (int) (var3 * 256.0D); - int var10 = (int) (var5 * 256.0D); - int var11 = (int) (var7 * 256.0D); - return var11 + (var10 << 8) + (var9 << 16); - } -} diff --git a/cache/src/main/java/net/runelite/cache/item/Graphics3D.java b/cache/src/main/java/net/runelite/cache/item/Graphics3D.java index 248ab6c572..85e599fe58 100644 --- a/cache/src/main/java/net/runelite/cache/item/Graphics3D.java +++ b/cache/src/main/java/net/runelite/cache/item/Graphics3D.java @@ -24,6 +24,8 @@ */ package net.runelite.cache.item; +import net.runelite.cache.models.JagexColor; + class Graphics3D extends Rasterizer2D { private static final double UNIT = Math.PI / 1024d; // How much of the circle each unit of SINE/COSINE is @@ -132,9 +134,9 @@ class Graphics3D extends Rasterizer2D Rasterizer3D_clipMidY2 = Rasterizer3D_clipHeight - centerY; } - public final void setBrightness(double var0) + public final void setBrightness(double brightness) { - colorPalette = new ColorPalette(var0, 0, 512).getColorPalette(); + colorPalette = JagexColor.createPalette(brightness); } final void rasterGouraud(int var0, int var1, int var2, int var3, int var4, int var5, int var6, int var7, int var8) diff --git a/cache/src/main/java/net/runelite/cache/item/ItemSpriteFactory.java b/cache/src/main/java/net/runelite/cache/item/ItemSpriteFactory.java index 6768f69f70..42fd81344d 100644 --- a/cache/src/main/java/net/runelite/cache/item/ItemSpriteFactory.java +++ b/cache/src/main/java/net/runelite/cache/item/ItemSpriteFactory.java @@ -33,6 +33,7 @@ import net.runelite.cache.definitions.providers.ModelProvider; import net.runelite.cache.definitions.providers.SpriteProvider; import net.runelite.cache.definitions.providers.TextureProvider; import net.runelite.cache.models.FaceNormal; +import net.runelite.cache.models.JagexColor; import net.runelite.cache.models.VertexNormal; public class ItemSpriteFactory @@ -111,7 +112,7 @@ public class ItemSpriteFactory SpritePixels spritePixels = new SpritePixels(36, 32); Graphics3D graphics = new Graphics3D(rsTextureProvider); - graphics.setBrightness(0.6d); + graphics.setBrightness(JagexColor.BRIGHTNESS_MAX); graphics.setRasterBuffer(spritePixels.pixels, 36, 32); graphics.reset(); graphics.setRasterClipping(); diff --git a/cache/src/main/java/net/runelite/cache/models/JagexColor.java b/cache/src/main/java/net/runelite/cache/models/JagexColor.java index 993073bf90..753bb53d29 100644 --- a/cache/src/main/java/net/runelite/cache/models/JagexColor.java +++ b/cache/src/main/java/net/runelite/cache/models/JagexColor.java @@ -29,7 +29,7 @@ public final class JagexColor public static final double BRIGHTNESS_MAX = .6; public static final double BRIGHTNESS_HIGH = .7; public static final double BRIGHTNESS_LOW = .8; - public static final double BRIGTHNESS_MIN = .9; + public static final double BRIGHTNESS_MIN = .9; private static final double HUE_OFFSET = (.5 / 64.D); private static final double SATURATION_OFFSET = (.5 / 8.D); @@ -133,4 +133,14 @@ public final class JagexColor | ((int) (g * 256.0D) << 8) | (int) (b * 256.0D); } + + public static int[] createPalette(double brightness) + { + int[] colorPalette = new int[65536]; + for (int i = 0; i < colorPalette.length; i++) + { + colorPalette[i] = HSLtoRGB((short) i, brightness); + } + return colorPalette; + } } \ No newline at end of file diff --git a/cache/src/main/java/net/runelite/cache/models/ObjExporter.java b/cache/src/main/java/net/runelite/cache/models/ObjExporter.java index 75b67f689f..c2f141b609 100644 --- a/cache/src/main/java/net/runelite/cache/models/ObjExporter.java +++ b/cache/src/main/java/net/runelite/cache/models/ObjExporter.java @@ -31,7 +31,7 @@ import net.runelite.cache.definitions.TextureDefinition; public class ObjExporter { - private static final double BRIGHTNESS = JagexColor.BRIGTHNESS_MIN; + private static final double BRIGHTNESS = JagexColor.BRIGHTNESS_MIN; private final TextureManager textureManager; private final ModelDefinition model; diff --git a/cache/src/test/java/net/runelite/cache/models/JagexColorTest.java b/cache/src/test/java/net/runelite/cache/models/JagexColorTest.java index 25291d4528..a4d7555613 100644 --- a/cache/src/test/java/net/runelite/cache/models/JagexColorTest.java +++ b/cache/src/test/java/net/runelite/cache/models/JagexColorTest.java @@ -30,7 +30,7 @@ import org.junit.Test; public class JagexColorTest { private static final double[] BRIGHTNESS_LEVELS = { - JagexColor.BRIGTHNESS_MIN, + JagexColor.BRIGHTNESS_MIN, JagexColor.BRIGHTNESS_LOW, JagexColor.BRIGHTNESS_HIGH, JagexColor.BRIGHTNESS_MAX, From a97d5b7017af2a4d2ef501c3f94070ebc2c2bc00 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sat, 16 Oct 2021 23:27:27 -0600 Subject: [PATCH 02/29] cache/RegionLoader: allow plugging an external xtea key provider --- .../net/runelite/cache/HeightMapDumper.java | 5 +- .../net/runelite/cache/MapImageDumper.java | 18 ++++-- .../runelite/cache/region/RegionLoader.java | 62 ++++++++++++------- .../net/runelite/cache/util/KeyProvider.java | 30 +++++++++ .../runelite/cache/util/XteaKeyManager.java | 5 +- .../runelite/cache/HeightMapDumperTest.java | 2 +- .../net/runelite/cache/MapDumperTest.java | 4 +- .../runelite/cache/MapImageDumperTest.java | 13 +++- 8 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 cache/src/main/java/net/runelite/cache/util/KeyProvider.java diff --git a/cache/src/main/java/net/runelite/cache/HeightMapDumper.java b/cache/src/main/java/net/runelite/cache/HeightMapDumper.java index 5db30815ff..1e855754e1 100644 --- a/cache/src/main/java/net/runelite/cache/HeightMapDumper.java +++ b/cache/src/main/java/net/runelite/cache/HeightMapDumper.java @@ -30,6 +30,7 @@ import java.io.IOException; import net.runelite.cache.fs.Store; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; +import net.runelite.cache.util.KeyProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,9 +49,9 @@ public class HeightMapDumper this.store = store; } - public void load() throws IOException + public void load(KeyProvider keyProvider) throws IOException { - regionLoader = new RegionLoader(store); + regionLoader = new RegionLoader(store, keyProvider); regionLoader.loadRegions(); regionLoader.calculateBounds(); } diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 833b4c1fc1..230953cab3 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -53,6 +53,7 @@ import net.runelite.cache.region.Location; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; import net.runelite.cache.util.Djb2; +import net.runelite.cache.util.KeyProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,7 +80,7 @@ public class MapImageDumper private final Map overlays = new HashMap<>(); private final Map scaledMapIcons = new HashMap<>(); - private RegionLoader regionLoader; + private final RegionLoader regionLoader; private final AreaManager areas; private final SpriteManager sprites; private RSTextureProvider rsTextureProvider; @@ -93,12 +94,18 @@ public class MapImageDumper @Setter private boolean outlineRegions; - public MapImageDumper(Store store) + public MapImageDumper(Store store, KeyProvider keyProvider) + { + this(store, new RegionLoader(store, keyProvider)); + } + + public MapImageDumper(Store store, RegionLoader regionLoader) { this.store = store; + this.regionLoader = regionLoader; this.areas = new AreaManager(store); this.sprites = new SpriteManager(store); - objectManager = new ObjectManager(store); + this.objectManager = new ObjectManager(store); } public void load() throws IOException @@ -111,7 +118,7 @@ public class MapImageDumper textureManager.load(); rsTextureProvider = new RSTextureProvider(textureManager, sprites); - loadRegions(store); + loadRegions(); areas.load(); sprites.load(); loadSprites(); @@ -891,9 +898,8 @@ public class MapImageDumper } } - private void loadRegions(Store store) throws IOException + private void loadRegions() throws IOException { - regionLoader = new RegionLoader(store); regionLoader.loadRegions(); regionLoader.calculateBounds(); diff --git a/cache/src/main/java/net/runelite/cache/region/RegionLoader.java b/cache/src/main/java/net/runelite/cache/region/RegionLoader.java index 21f4868a9a..6e0687e826 100644 --- a/cache/src/main/java/net/runelite/cache/region/RegionLoader.java +++ b/cache/src/main/java/net/runelite/cache/region/RegionLoader.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import lombok.extern.slf4j.Slf4j; import net.runelite.cache.IndexType; import net.runelite.cache.definitions.LocationsDefinition; import net.runelite.cache.definitions.MapDefinition; @@ -37,39 +38,44 @@ import net.runelite.cache.fs.Archive; import net.runelite.cache.fs.Index; import net.runelite.cache.fs.Storage; import net.runelite.cache.fs.Store; -import net.runelite.cache.util.XteaKeyManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.runelite.cache.util.KeyProvider; +@Slf4j public class RegionLoader { - private static final Logger logger = LoggerFactory.getLogger(RegionLoader.class); - private static final int MAX_REGION = 32768; private final Store store; private final Index index; - private final XteaKeyManager keyManager; + private final KeyProvider keyProvider; private final Map regions = new HashMap<>(); private Region lowestX = null, lowestY = null; private Region highestX = null, highestY = null; - public RegionLoader(Store store) + public RegionLoader(Store store, KeyProvider keyProvider) { this.store = store; index = store.getIndex(IndexType.MAPS); - keyManager = new XteaKeyManager(); + this.keyProvider = keyProvider; } public void loadRegions() throws IOException { + if (!this.regions.isEmpty()) + { + return; + } + for (int i = 0; i < MAX_REGION; ++i) { - Region region = this.loadRegionFromArchive(i); - if (region != null) + try { - regions.put(i, region); + this.loadRegionFromArchive(i); + } + catch (IOException ex) + { + log.debug("Can't decrypt region " + i, ex); } } } @@ -97,24 +103,31 @@ public class RegionLoader Region region = new Region(i); region.loadTerrain(mapDef); - int[] keys = keyManager.getKeys(i); + int[] keys = keyProvider.getKey(i); if (keys != null) { - try - { - data = land.decompress(storage.loadArchive(land), keys); - LocationsDefinition locDef = new LocationsLoader().load(x, y, data); - region.loadLocations(locDef); - } - catch (IOException ex) - { - logger.debug("Can't decrypt region " + i, ex); - } + data = land.decompress(storage.loadArchive(land), keys); + LocationsDefinition locDef = new LocationsLoader().load(x, y, data); + region.loadLocations(locDef); } + regions.put(i, region); + return region; } + public Region loadRegion(int id, MapDefinition map, LocationsDefinition locs) + { + Region r = new Region(id); + r.loadTerrain(map); + if (locs != null) + { + r.loadLocations(locs); + } + regions.put(id, r); + return r; + } + public void calculateBounds() { for (Region region : regions.values()) @@ -153,6 +166,11 @@ public class RegionLoader return regions.get((x << 8) | y); } + public Region findRegionForRegionCoordinates(int x, int y) + { + return regions.get((x << 8) | y); + } + public Region getLowestX() { return lowestX; diff --git a/cache/src/main/java/net/runelite/cache/util/KeyProvider.java b/cache/src/main/java/net/runelite/cache/util/KeyProvider.java new file mode 100644 index 0000000000..498f51d5a7 --- /dev/null +++ b/cache/src/main/java/net/runelite/cache/util/KeyProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 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.cache.util; + +public interface KeyProvider +{ + int[] getKey(int region); +} diff --git a/cache/src/main/java/net/runelite/cache/util/XteaKeyManager.java b/cache/src/main/java/net/runelite/cache/util/XteaKeyManager.java index c1c7607e9b..669bb6a02d 100644 --- a/cache/src/main/java/net/runelite/cache/util/XteaKeyManager.java +++ b/cache/src/main/java/net/runelite/cache/util/XteaKeyManager.java @@ -35,7 +35,7 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class XteaKeyManager +public class XteaKeyManager implements KeyProvider { private static final Logger logger = LoggerFactory.getLogger(XteaKeyManager.class); @@ -56,7 +56,8 @@ public class XteaKeyManager logger.info("Loaded {} keys", keys.size()); } - public int[] getKeys(int region) + @Override + public int[] getKey(int region) { return keys.get(region); } diff --git a/cache/src/test/java/net/runelite/cache/HeightMapDumperTest.java b/cache/src/test/java/net/runelite/cache/HeightMapDumperTest.java index 92327f50d7..4b41b047aa 100644 --- a/cache/src/test/java/net/runelite/cache/HeightMapDumperTest.java +++ b/cache/src/test/java/net/runelite/cache/HeightMapDumperTest.java @@ -52,7 +52,7 @@ public class HeightMapDumperTest store.load(); HeightMapDumper dumper = new HeightMapDumper(store); - dumper.load(); + dumper.load(null); BufferedImage image = dumper.drawHeightMap(0); diff --git a/cache/src/test/java/net/runelite/cache/MapDumperTest.java b/cache/src/test/java/net/runelite/cache/MapDumperTest.java index b73c250e43..b822673988 100644 --- a/cache/src/test/java/net/runelite/cache/MapDumperTest.java +++ b/cache/src/test/java/net/runelite/cache/MapDumperTest.java @@ -75,7 +75,7 @@ public class MapDumperTest for (int i = 0; i < MAX_REGIONS; i++) { - int[] keys = keyManager.getKeys(i); + int[] keys = keyManager.getKey(i); int x = i >> 8; int y = i & 0xFF; @@ -140,7 +140,7 @@ public class MapDumperTest MapDefinition mapDef = new MapLoader().load(x, y, data); LocationsDefinition locDef = null; - int[] keys = keyManager.getKeys(i); + int[] keys = keyManager.getKey(i); if (keys != null) { try diff --git a/cache/src/test/java/net/runelite/cache/MapImageDumperTest.java b/cache/src/test/java/net/runelite/cache/MapImageDumperTest.java index 7491e54b58..89b930673f 100644 --- a/cache/src/test/java/net/runelite/cache/MapImageDumperTest.java +++ b/cache/src/test/java/net/runelite/cache/MapImageDumperTest.java @@ -31,6 +31,7 @@ import javax.imageio.ImageIO; import net.runelite.cache.fs.Store; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; +import net.runelite.cache.util.XteaKeyManager; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -56,7 +57,10 @@ public class MapImageDumperTest { store.load(); - MapImageDumper dumper = new MapImageDumper(store); + XteaKeyManager keyManager = new XteaKeyManager(); + keyManager.loadKeys(null); + + MapImageDumper dumper = new MapImageDumper(store, keyManager); dumper.load(); for (int i = 0; i < Region.Z; ++i) @@ -82,10 +86,13 @@ public class MapImageDumperTest { store.load(); - RegionLoader regionLoader = new RegionLoader(store); + XteaKeyManager keyManager = new XteaKeyManager(); + keyManager.loadKeys(null); + + RegionLoader regionLoader = new RegionLoader(store, keyManager); regionLoader.loadRegions(); - MapImageDumper dumper = new MapImageDumper(store); + MapImageDumper dumper = new MapImageDumper(store, regionLoader); dumper.load(); int z = 0; From a00a8f3925798d84c04869275aea1d4f3db2579d Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sun, 17 Oct 2021 00:15:48 -0600 Subject: [PATCH 03/29] cache/MapImageDumper: add layer & brightness configuration --- .../net/runelite/cache/MapImageDumper.java | 65 +++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 230953cab3..2d159ef362 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -33,6 +33,8 @@ import java.util.HashMap; import java.util.Map; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; import net.runelite.cache.definitions.AreaDefinition; import net.runelite.cache.definitions.ObjectDefinition; import net.runelite.cache.definitions.OverlayDefinition; @@ -54,13 +56,11 @@ import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; import net.runelite.cache.util.Djb2; import net.runelite.cache.util.KeyProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +@Slf4j +@Accessors(chain = true) public class MapImageDumper { - private static final Logger logger = LoggerFactory.getLogger(MapImageDumper.class); - private static final int MAP_SCALE = 4; // this squared is the number of pixels per map square private static final int MAPICON_MAX_WIDTH = 5; // scale minimap icons down to this size so they fit.. private static final int MAPICON_MAX_HEIGHT = 6; @@ -71,8 +71,8 @@ public class MapImageDumper private static int[][] TILE_SHAPE_2D = new int[][]{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}, {1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1}, {1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1}}; private static int[][] TILE_ROTATION_2D = new int[][]{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, {12, 8, 4, 0, 13, 9, 5, 1, 14, 10, 6, 2, 15, 11, 7, 3}, {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}, {3, 7, 11, 15, 2, 6, 10, 14, 1, 5, 9, 13, 0, 4, 8, 12}}; - private final int wallColor = (238 + (int) (Math.random() * 20.0D) - 10 << 16) + (238 + (int) (Math.random() * 20.0D) - 10 << 8) + (238 + (int) (Math.random() * 20.0D) - 10); - private final int doorColor = 238 + (int) (Math.random() * 20.0D) - 10 << 16; + private final int wallColor = (238 + (int) (random() * 20.0D) - 10 << 16) + (238 + (int) (random() * 20.0D) - 10 << 8) + (238 + (int) (random() * 20.0D) - 10); + private final int doorColor = 238 + (int) (random() * 20.0D) - 10 << 16; private final Store store; @@ -94,6 +94,18 @@ public class MapImageDumper @Setter private boolean outlineRegions; + @Getter + @Setter + private boolean renderMap = true; + + @Getter + @Setter + private boolean renderObjects = true; + + @Getter + @Setter + private boolean renderIcons = true; + public MapImageDumper(Store store, KeyProvider keyProvider) { this(store, new RegionLoader(store, keyProvider)); @@ -108,7 +120,19 @@ public class MapImageDumper this.objectManager = new ObjectManager(store); } - public void load() throws IOException + protected double random() + { + // the client would use a random value here, but we prefer determinism + return 0.5; + } + + public MapImageDumper setBrightness(double brightness) + { + colorPalette = JagexColor.createPalette(brightness); + return this; + } + + public MapImageDumper load() throws IOException { loadUnderlays(store); loadOverlays(store); @@ -122,6 +146,8 @@ public class MapImageDumper areas.load(); sprites.load(); loadSprites(); + + return this; } public BufferedImage drawMap(int z) @@ -138,7 +164,7 @@ public class MapImageDumper int pixelsX = dimX * MAP_SCALE; int pixelsY = dimY * MAP_SCALE; - logger.info("Map image dimensions: {}px x {}px, {}px per map square ({} MB). Max memory: {}mb", pixelsX, pixelsY, + log.info("Map image dimensions: {}px x {}px, {}px per map square ({} MB). Max memory: {}mb", pixelsX, pixelsY, MAP_SCALE, (pixelsX * pixelsY * 3 / 1024 / 1024), Runtime.getRuntime().maxMemory() / 1024L / 1024L); @@ -167,6 +193,11 @@ public class MapImageDumper private void drawMap(BufferedImage image, int drawBaseX, int drawBaseY, int z, Region region) { + if (!renderMap) + { + return; + } + int[][] map = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE]; drawMap(map, region, z); @@ -493,6 +524,11 @@ public class MapImageDumper private void drawObjects(BufferedImage image, int drawBaseX, int drawBaseY, Region region, int z) { + if (!renderObjects) + { + return; + } + Graphics2D graphics = image.createGraphics(); for (Location location : region.getLocations()) @@ -863,6 +899,11 @@ public class MapImageDumper private void drawMapIcons(Graphics2D graphics, Region region, int z, int drawBaseX, int drawBaseY) { + if (!renderIcons) + { + return; + } + for (Location location : region.getLocations()) { int localZ = location.getPosition().getZ(); @@ -903,10 +944,10 @@ public class MapImageDumper regionLoader.loadRegions(); regionLoader.calculateBounds(); - logger.info("North most region: {}", regionLoader.getLowestY().getBaseY()); - logger.info("South most region: {}", regionLoader.getHighestY().getBaseY()); - logger.info("West most region: {}", regionLoader.getLowestX().getBaseX()); - logger.info("East most region: {}", regionLoader.getHighestX().getBaseX()); + log.debug("North most region: {}", regionLoader.getLowestY().getBaseY()); + log.debug("South most region: {}", regionLoader.getHighestY().getBaseY()); + log.debug("West most region: {}", regionLoader.getLowestX().getBaseX()); + log.debug("East most region: {}", regionLoader.getHighestX().getBaseX()); } private void loadUnderlays(Store store) throws IOException From 050e453a64fc237d66d3e7a3ab16d91833176eae Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sun, 17 Oct 2021 02:13:01 -0600 Subject: [PATCH 04/29] cache/MapImageDumper: add transparency support --- .../net/runelite/cache/MapImageDumper.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 2d159ef362..171576d743 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -106,6 +106,10 @@ public class MapImageDumper @Setter private boolean renderIcons = true; + @Getter + @Setter + private boolean transparency = false; + public MapImageDumper(Store store, KeyProvider keyProvider) { this(store, new RegionLoader(store, keyProvider)); @@ -168,7 +172,7 @@ public class MapImageDumper MAP_SCALE, (pixelsX * pixelsY * 3 / 1024 / 1024), Runtime.getRuntime().maxMemory() / 1024L / 1024L); - BufferedImage image = new BufferedImage(pixelsX, pixelsY, BufferedImage.TYPE_INT_RGB); + BufferedImage image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); drawMap(image, z); drawObjects(image, z); @@ -182,7 +186,7 @@ public class MapImageDumper int pixelsX = Region.X * MAP_SCALE; int pixelsY = Region.Y * MAP_SCALE; - BufferedImage image = new BufferedImage(pixelsX, pixelsY, BufferedImage.TYPE_INT_RGB); + BufferedImage image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); drawMap(image, 0, 0, z, region); drawObjects(image, 0, 0, region, z); @@ -252,9 +256,13 @@ public class MapImageDumper { for (int j = 0; j < MAP_SCALE; ++j) { - to.setRGB(drawBaseX * MAP_SCALE + x * MAP_SCALE + i, - drawBaseY * MAP_SCALE + y * MAP_SCALE + j, - pixels[x * MAP_SCALE + i][y * MAP_SCALE + j]); + int argb = pixels[x * MAP_SCALE + i][y * MAP_SCALE + j]; + if (argb != 0) + { + to.setRGB(drawBaseX * MAP_SCALE + x * MAP_SCALE + i, + drawBaseY * MAP_SCALE + y * MAP_SCALE + j, + argb); + } } } } @@ -383,11 +391,11 @@ public class MapImageDumper if (underlayHsl != -1) { int var0 = method1792(underlayHsl, 96); - underlayRgb = colorPalette[var0]; + underlayRgb = colorPalette[var0] | 0xFF000000; } int shape, rotation; - Integer overlayRgb = null; + int overlayRgb = 0; if (overlayId == 0) { shape = rotation = 0; @@ -399,28 +407,27 @@ public class MapImageDumper OverlayDefinition overlayDefinition = findOverlay(overlayId - 1); int overlayTexture = overlayDefinition.getTexture(); - int rgb; + int hsl; if (overlayTexture >= 0) { - rgb = rsTextureProvider.getAverageTextureRGB(overlayTexture); + hsl = rsTextureProvider.getAverageTextureRGB(overlayTexture); } else if (overlayDefinition.getRgbColor() == 0xFF_00FF) { - rgb = -2; + hsl = -2; } else { // randomness added here int overlayHsl = packHsl(overlayDefinition.getHue(), overlayDefinition.getSaturation(), overlayDefinition.getLightness()); - rgb = overlayHsl; + hsl = overlayHsl; } - overlayRgb = 0; - if (rgb != -2) + if (hsl != -2) { - int var0 = adjustHSLListness0(rgb, 96); - overlayRgb = colorPalette[var0]; + int var0 = adjustHSLListness0(hsl, 96); + overlayRgb = colorPalette[var0] | 0xFF000000; } if (overlayDefinition.getSecondaryRgbColor() != -1) @@ -428,9 +435,9 @@ public class MapImageDumper int hue = overlayDefinition.getOtherHue(); int sat = overlayDefinition.getOtherSaturation(); int olight = overlayDefinition.getOtherLightness(); - rgb = packHsl(hue, sat, olight); - int var0 = adjustHSLListness0(rgb, 96); - overlayRgb = colorPalette[var0]; + hsl = packHsl(hue, sat, olight); + int var0 = adjustHSLListness0(hsl, 96); + overlayRgb = colorPalette[var0] | 0xFF000000; } } @@ -585,6 +592,7 @@ public class MapImageDumper { rgb = doorColor; } + rgb |= 0xFF000000; if (object.getMapSceneID() != -1) { @@ -698,10 +706,10 @@ public class MapImageDumper continue; } - int rgb = 0xEE_EEEE; + int rgb = 0xFFEE_EEEE; if (hash > 0) { - rgb = 0xEE_0000; + rgb = 0xFFEE_0000; } if (rotation != 0 && rotation != 2) From 685ee8d7e2361f68159f26a355d92a6e98d0c58d Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sun, 17 Oct 2021 03:49:22 -0600 Subject: [PATCH 05/29] cache/MapImageDumper: fix icon & wall size & placement --- .../net/runelite/cache/MapImageDumper.java | 88 +++++++++---------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 171576d743..d20ede9682 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -26,7 +26,6 @@ package net.runelite.cache; import java.awt.Color; import java.awt.Graphics2D; -import java.awt.Image; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.HashMap; @@ -54,7 +53,6 @@ import net.runelite.cache.models.JagexColor; import net.runelite.cache.region.Location; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; -import net.runelite.cache.util.Djb2; import net.runelite.cache.util.KeyProvider; @Slf4j @@ -62,8 +60,6 @@ import net.runelite.cache.util.KeyProvider; public class MapImageDumper { private static final int MAP_SCALE = 4; // this squared is the number of pixels per map square - private static final int MAPICON_MAX_WIDTH = 5; // scale minimap icons down to this size so they fit.. - private static final int MAPICON_MAX_HEIGHT = 6; private static final int BLEND = 5; // number of surrounding tiles for ground blending private static int[] colorPalette = JagexColor.createPalette(JagexColor.BRIGHTNESS_MIN); @@ -78,7 +74,7 @@ public class MapImageDumper private final Map underlays = new HashMap<>(); private final Map overlays = new HashMap<>(); - private final Map scaledMapIcons = new HashMap<>(); + private SpriteDefinition[] mapDecorations; private final RegionLoader regionLoader; private final AreaManager areas; @@ -536,8 +532,6 @@ public class MapImageDumper return; } - Graphics2D graphics = image.createGraphics(); - for (Location location : region.getLocations()) { @@ -576,7 +570,7 @@ public class MapImageDumper ObjectDefinition object = findObject(location.getId()); int drawX = (drawBaseX + localX) * MAP_SCALE; - int drawY = (drawBaseY + (Region.Y - 1 - localY)) * MAP_SCALE; + int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; if (type >= 0 && type <= 3) { @@ -596,8 +590,7 @@ public class MapImageDumper if (object.getMapSceneID() != -1) { - Image spriteImage = scaledMapIcons.get(object.getMapSceneID()); - graphics.drawImage(spriteImage, drawX * MAP_SCALE, drawY * MAP_SCALE, null); + blitMapDecoration(image, drawX, drawY, object); } else { @@ -690,8 +683,7 @@ public class MapImageDumper { if (object.getMapSceneID() != -1) { - Image spriteImage = scaledMapIcons.get(object.getMapSceneID()); - graphics.drawImage(spriteImage, drawX, drawY, null); + blitMapDecoration(image, drawX, drawY, object); continue; } @@ -732,13 +724,10 @@ public class MapImageDumper // ground object if (object.getMapSceneID() != -1) { - Image spriteImage = scaledMapIcons.get(object.getMapSceneID()); - graphics.drawImage(spriteImage, drawX, drawY, null); + blitMapDecoration(image, drawX, drawY, object); } } } - - graphics.dispose(); } private void drawObjects(BufferedImage image, int z) @@ -766,7 +755,7 @@ public class MapImageDumper Graphics2D graphics = image.createGraphics(); - drawMapIcons(graphics, region, z, drawBaseX, drawBaseY); + drawMapIcons(image, region, z, drawBaseX, drawBaseY); if (labelRegions) { @@ -905,7 +894,7 @@ public class MapImageDumper } } - private void drawMapIcons(Graphics2D graphics, Region region, int z, int drawBaseX, int drawBaseY) + private void drawMapIcons(BufferedImage img, Region region, int z, int drawBaseX, int drawBaseY) { if (!renderIcons) { @@ -936,13 +925,13 @@ public class MapImageDumper AreaDefinition area = areas.getArea(od.getMapAreaId()); assert area != null; - int spriteId = area.spriteId; - - SpriteDefinition sprite = sprites.findSprite(spriteId, 0); + SpriteDefinition sprite = sprites.findSprite(area.spriteId, 0); assert sprite != null; - BufferedImage iconImage = sprites.getSpriteImage(sprite); - graphics.drawImage(iconImage, drawX * MAP_SCALE, drawY * MAP_SCALE, null); + blitIcon(img, + 2 + (drawX * MAP_SCALE) - (sprite.getMaxWidth() / 2), + 2 + (drawY * MAP_SCALE) - (sprite.getMaxHeight() / 2), + sprite); } } } @@ -1008,35 +997,42 @@ public class MapImageDumper { Storage storage = store.getStorage(); Index index = store.getIndex(IndexType.SPRITES); - final int mapsceneHash = Djb2.hash("mapscene"); + Archive a = index.findArchiveByName("mapscene"); + byte[] contents = a.decompress(storage.loadArchive(a)); - for (Archive a : index.getArchives()) + SpriteLoader loader = new SpriteLoader(); + mapDecorations = loader.load(a.getArchiveId(), contents); + } + + private void blitMapDecoration(BufferedImage dst, int x, int y, ObjectDefinition object) + { + SpriteDefinition sprite = mapDecorations[object.getMapSceneID()]; + int ox = (object.getSizeX() * MAP_SCALE - sprite.getWidth()) / 2; + int oy = (object.getSizeY() * MAP_SCALE - sprite.getHeight()) / 2; + blitIcon(dst, x + ox, y + oy, sprite); + } + + private void blitIcon(BufferedImage dst, int x, int y, SpriteDefinition sprite) + { + x += sprite.getOffsetX(); + y += sprite.getOffsetY(); + + int ymin = Math.max(0, -y); + int ymax = Math.min(sprite.getHeight(), dst.getHeight() - y); + + int xmin = Math.max(0, -x); + int xmax = Math.min(sprite.getWidth(), dst.getWidth() - x); + + for (int yo = ymin; yo < ymax; yo++) { - byte[] contents = a.decompress(storage.loadArchive(a)); - - SpriteLoader loader = new SpriteLoader(); - SpriteDefinition[] sprites = loader.load(a.getArchiveId(), contents); - - for (SpriteDefinition sprite : sprites) + for (int xo = xmin; xo < xmax; xo++) { - if (sprite.getHeight() <= 0 || sprite.getWidth() <= 0) + int rgb = sprite.getPixels()[xo + (yo * sprite.getWidth())]; + if (rgb != 0) { - continue; - } - - if (a.getNameHash() == mapsceneHash) - { - BufferedImage spriteImage = new BufferedImage(sprite.getWidth(), sprite.getHeight(), BufferedImage.TYPE_INT_ARGB); - spriteImage.setRGB(0, 0, sprite.getWidth(), sprite.getHeight(), sprite.getPixels(), 0, sprite.getWidth()); - - // scale image down so it fits - Image scaledImage = spriteImage.getScaledInstance(MAPICON_MAX_WIDTH, MAPICON_MAX_HEIGHT, 0); - - assert scaledMapIcons.containsKey(sprite.getFrame()) == false; - scaledMapIcons.put(sprite.getFrame(), scaledImage); + dst.setRGB(x + xo, y + yo, rgb | 0xFF000000); } } } } - } From 3ca476094ca0522c7f4e2c4ead3f44df2c72bb0e Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sun, 17 Oct 2021 03:06:21 -0600 Subject: [PATCH 06/29] cache/MapImageDumper: handle pushdown objects correctly --- .../net/runelite/cache/MapImageDumper.java | 437 ++++++++++-------- 1 file changed, 243 insertions(+), 194 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index d20ede9682..6973dc86af 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -28,7 +28,10 @@ import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.Getter; import lombok.Setter; @@ -51,6 +54,7 @@ import net.runelite.cache.fs.Store; import net.runelite.cache.item.RSTextureProvider; import net.runelite.cache.models.JagexColor; import net.runelite.cache.region.Location; +import net.runelite.cache.region.Position; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; import net.runelite.cache.util.KeyProvider; @@ -198,31 +202,36 @@ public class MapImageDumper return; } - int[][] map = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE]; - drawMap(map, region, z); - - int[][] above = null; - if (z < 3) - { - above = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE]; - drawMap(above, region, z + 1); - } + int[][][] map = new int[4][][]; for (int x = 0; x < Region.X; ++x) { for (int y = 0; y < Region.Y; ++y) { boolean isBridge = (region.getTileSetting(1, x, Region.Y - y - 1) & 2) != 0; - - int tileSetting = region.getTileSetting(z, x, Region.Y - y - 1); - if (!isBridge && ((tileSetting & 24) == 0)) + int tileZ = z + (isBridge ? 1 : 0); + if (tileZ >= Region.Z) { - drawTile(image, map, drawBaseX, drawBaseY, x, y); + continue; } - if (z < 3 && isBridge) // client also has a check for &8 != 0 here + int tileSetting = region.getTileSetting(z, x, Region.Y - y - 1); + if ((tileSetting & 24) == 0) { - drawTile(image, above, drawBaseX, drawBaseY, x, y); + if (z == 0 && isBridge) + { + drawTile(image, map, region, drawBaseX, drawBaseY, 0, x, y); + } + drawTile(image, map, region, drawBaseX, drawBaseY, tileZ, x, y); + } + + if (tileZ < 3) + { + int upTileSetting = region.getTileSetting(z + 1, x, Region.Y - y - 1); + if ((upTileSetting & 8) != 0) + { + drawTile(image, map, region, drawBaseX, drawBaseY, tileZ + 1, x, y); + } } } } @@ -246,8 +255,16 @@ public class MapImageDumper } } - private void drawTile(BufferedImage to, int[][] pixels, int drawBaseX, int drawBaseY, int x, int y) + private void drawTile(BufferedImage to, int[][][] planes, Region region, int drawBaseX, int drawBaseY, int z, int x, int y) { + int[][] pixels = planes[z]; + + if (pixels == null) + { + pixels = planes[z] = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE]; + drawMap(pixels, region, z); + } + for (int i = 0; i < MAP_SCALE; ++i) { for (int j = 0; j < MAP_SCALE; ++j) @@ -532,201 +549,231 @@ public class MapImageDumper return; } - for (Location location : region.getLocations()) + List planeLocs = new ArrayList<>(); + List pushDownLocs = new ArrayList<>(); + List> layers = Arrays.asList(planeLocs, pushDownLocs); + for (int localX = 0; localX < Region.X; localX++) { - - int rotation = location.getOrientation(); - int type = location.getType(); - - int localX = location.getPosition().getX() - region.getBaseX(); - int localY = location.getPosition().getY() - region.getBaseY(); - - boolean isBridge = (region.getTileSetting(1, localX, localY) & 2) != 0; - - if (location.getPosition().getZ() == z + 1) + int regionX = localX + region.getBaseX(); + for (int localY = 0; localY < Region.Y; localY++) { - if (!isBridge) - { - continue; - } - } - else if (location.getPosition().getZ() == z) - { - if (isBridge) - { - continue; - } + int regionY = localY + region.getBaseY(); - if ((region.getTileSetting(z, localX, localY) & 24) != 0) - { - continue; - } - } - else - { - continue; - } + planeLocs.clear(); + pushDownLocs.clear(); + boolean isBridge = (region.getTileSetting(1, localX, localY) & 2) != 0; + int tileZ = z + (isBridge ? 1 : 0); - ObjectDefinition object = findObject(location.getId()); - - int drawX = (drawBaseX + localX) * MAP_SCALE; - int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; - - if (type >= 0 && type <= 3) - { - // this is a wall - int hash = (localY << 7) + localX + (location.getId() << 14) + 0x4000_0000; - if (object.getWallOrDoor() == 0) + for (Location loc : region.getLocations()) { - hash -= Integer.MIN_VALUE; - } - - int rgb = wallColor; - if (hash > 0) - { - rgb = doorColor; - } - rgb |= 0xFF000000; - - if (object.getMapSceneID() != -1) - { - blitMapDecoration(image, drawX, drawY, object); - } - else - { - if (type == 0 || type == 2) + Position pos = loc.getPosition(); + if (pos.getX() != regionX || pos.getY() != regionY) { - if (rotation == 0) + continue; + } + + if (pos.getZ() == tileZ && (region.getTileSetting(z, localX, localY) & 24) == 0) + { + planeLocs.add(loc); + } + else if (z < 3 && pos.getZ() == tileZ + 1 && (region.getTileSetting(z + 1, localX, localY) & 8) != 0) + { + pushDownLocs.add(loc); + } + } + + for (List locs : layers) + { + for (Location location : locs) + { + int type = location.getType(); + if (type >= 0 && type <= 3) { - image.setRGB(drawX + 0, drawY + 0, rgb); - image.setRGB(drawX + 0, drawY + 1, rgb); - image.setRGB(drawX + 0, drawY + 2, rgb); - image.setRGB(drawX + 0, drawY + 3, rgb); - } - else if (rotation == 1) - { - image.setRGB(drawX + 0, drawY + 0, rgb); - image.setRGB(drawX + 1, drawY + 0, rgb); - image.setRGB(drawX + 2, drawY + 0, rgb); - image.setRGB(drawX + 3, drawY + 0, rgb); - } - else if (rotation == 2) - { - image.setRGB(drawX + 3, drawY + 0, rgb); - image.setRGB(drawX + 3, drawY + 1, rgb); - image.setRGB(drawX + 3, drawY + 2, rgb); - image.setRGB(drawX + 3, drawY + 3, rgb); - } - else if (rotation == 3) - { - image.setRGB(drawX + 0, drawY + 3, rgb); - image.setRGB(drawX + 1, drawY + 3, rgb); - image.setRGB(drawX + 2, drawY + 3, rgb); - image.setRGB(drawX + 3, drawY + 3, rgb); + int rotation = location.getOrientation(); + + ObjectDefinition object = findObject(location.getId()); + + int drawX = (drawBaseX + localX) * MAP_SCALE; + int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; + + // this is a wall + int hash = (localY << 7) + localX + (location.getId() << 14) + 0x4000_0000; + if (object.getWallOrDoor() == 0) + { + hash -= Integer.MIN_VALUE; + } + + int rgb = wallColor; + if (hash > 0) + { + rgb = doorColor; + } + rgb |= 0xFF000000; + + if (object.getMapSceneID() != -1) + { + blitMapDecoration(image, drawX, drawY, object); + } + else + { + if (type == 0 || type == 2) + { + if (rotation == 0) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + image.setRGB(drawX + 0, drawY + 1, rgb); + image.setRGB(drawX + 0, drawY + 2, rgb); + image.setRGB(drawX + 0, drawY + 3, rgb); + } + else if (rotation == 1) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + image.setRGB(drawX + 1, drawY + 0, rgb); + image.setRGB(drawX + 2, drawY + 0, rgb); + image.setRGB(drawX + 3, drawY + 0, rgb); + } + else if (rotation == 2) + { + image.setRGB(drawX + 3, drawY + 0, rgb); + image.setRGB(drawX + 3, drawY + 1, rgb); + image.setRGB(drawX + 3, drawY + 2, rgb); + image.setRGB(drawX + 3, drawY + 3, rgb); + } + else if (rotation == 3) + { + image.setRGB(drawX + 0, drawY + 3, rgb); + image.setRGB(drawX + 1, drawY + 3, rgb); + image.setRGB(drawX + 2, drawY + 3, rgb); + image.setRGB(drawX + 3, drawY + 3, rgb); + } + } + + if (type == 3) + { + if (rotation == 0) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + } + else if (rotation == 1) + { + image.setRGB(drawX + 3, drawY + 0, rgb); + } + else if (rotation == 2) + { + image.setRGB(drawX + 3, drawY + 3, rgb); + } + else if (rotation == 3) + { + image.setRGB(drawX + 0, drawY + 3, rgb); + } + } + + if (type == 2) + { + if (rotation == 3) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + image.setRGB(drawX + 0, drawY + 1, rgb); + image.setRGB(drawX + 0, drawY + 2, rgb); + image.setRGB(drawX + 0, drawY + 3, rgb); + } + else if (rotation == 0) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + image.setRGB(drawX + 1, drawY + 0, rgb); + image.setRGB(drawX + 2, drawY + 0, rgb); + image.setRGB(drawX + 3, drawY + 0, rgb); + } + else if (rotation == 1) + { + image.setRGB(drawX + 3, drawY + 0, rgb); + image.setRGB(drawX + 3, drawY + 1, rgb); + image.setRGB(drawX + 3, drawY + 2, rgb); + image.setRGB(drawX + 3, drawY + 3, rgb); + } + else if (rotation == 2) + { + image.setRGB(drawX + 0, drawY + 3, rgb); + image.setRGB(drawX + 1, drawY + 3, rgb); + image.setRGB(drawX + 2, drawY + 3, rgb); + image.setRGB(drawX + 3, drawY + 3, rgb); + } + } + } } } - if (type == 3) + for (Location location : locs) { - if (rotation == 0) + int type = location.getType(); + if (type == 9) { - image.setRGB(drawX + 0, drawY + 0, rgb); - } - else if (rotation == 1) - { - image.setRGB(drawX + 3, drawY + 0, rgb); - } - else if (rotation == 2) - { - image.setRGB(drawX + 3, drawY + 3, rgb); - } - else if (rotation == 3) - { - image.setRGB(drawX + 0, drawY + 3, rgb); + int rotation = location.getOrientation(); + + ObjectDefinition object = findObject(location.getId()); + + int drawX = (drawBaseX + localX) * MAP_SCALE; + int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; + + if (object.getMapSceneID() != -1) + { + blitMapDecoration(image, drawX, drawY, object); + continue; + } + + int hash = (localY << 7) + localX + (location.getId() << 14) + 0x4000_0000; + if (object.getWallOrDoor() == 0) + { + hash -= Integer.MIN_VALUE; + } + + if ((hash >> 29 & 3) != 2) + { + continue; + } + + int rgb = 0xFFEE_EEEE; + if (hash > 0) + { + rgb = 0xFFEE_0000; + } + + if (rotation != 0 && rotation != 2) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + image.setRGB(drawX + 1, drawY + 1, rgb); + image.setRGB(drawX + 2, drawY + 2, rgb); + image.setRGB(drawX + 3, drawY + 3, rgb); + } + else + { + image.setRGB(drawX + 0, drawY + 3, rgb); + image.setRGB(drawX + 1, drawY + 2, rgb); + image.setRGB(drawX + 2, drawY + 1, rgb); + image.setRGB(drawX + 3, drawY + 0, rgb); + } } } - if (type == 2) + for (Location location : locs) { - if (rotation == 3) + int type = location.getType(); + if (type == 22 || (type >= 9 && type <= 11)) { - image.setRGB(drawX + 0, drawY + 0, rgb); - image.setRGB(drawX + 0, drawY + 1, rgb); - image.setRGB(drawX + 0, drawY + 2, rgb); - image.setRGB(drawX + 0, drawY + 3, rgb); - } - else if (rotation == 0) - { - image.setRGB(drawX + 0, drawY + 0, rgb); - image.setRGB(drawX + 1, drawY + 0, rgb); - image.setRGB(drawX + 2, drawY + 0, rgb); - image.setRGB(drawX + 3, drawY + 0, rgb); - } - else if (rotation == 1) - { - image.setRGB(drawX + 3, drawY + 0, rgb); - image.setRGB(drawX + 3, drawY + 1, rgb); - image.setRGB(drawX + 3, drawY + 2, rgb); - image.setRGB(drawX + 3, drawY + 3, rgb); - } - else if (rotation == 2) - { - image.setRGB(drawX + 0, drawY + 3, rgb); - image.setRGB(drawX + 1, drawY + 3, rgb); - image.setRGB(drawX + 2, drawY + 3, rgb); - image.setRGB(drawX + 3, drawY + 3, rgb); + ObjectDefinition object = findObject(location.getId()); + + int drawX = (drawBaseX + localX) * MAP_SCALE; + int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; + + // ground object + if (object.getMapSceneID() != -1) + { + blitMapDecoration(image, drawX, drawY, object); + } } } } } - else if (type == 9) - { - if (object.getMapSceneID() != -1) - { - blitMapDecoration(image, drawX, drawY, object); - continue; - } - - int hash = (localY << 7) + localX + (location.getId() << 14) + 0x4000_0000; - if (object.getWallOrDoor() == 0) - { - hash -= Integer.MIN_VALUE; - } - - if ((hash >> 29 & 3) != 2) - { - continue; - } - - int rgb = 0xFFEE_EEEE; - if (hash > 0) - { - rgb = 0xFFEE_0000; - } - - if (rotation != 0 && rotation != 2) - { - image.setRGB(drawX + 0, drawY + 0, rgb); - image.setRGB(drawX + 1, drawY + 1, rgb); - image.setRGB(drawX + 2, drawY + 2, rgb); - image.setRGB(drawX + 3, drawY + 3, rgb); - } - else - { - image.setRGB(drawX + 0, drawY + 3, rgb); - image.setRGB(drawX + 1, drawY + 2, rgb); - image.setRGB(drawX + 2, drawY + 1, rgb); - image.setRGB(drawX + 3, drawY + 0, rgb); - } - } - else if (type == 22 || (type >= 9 && type <= 11)) - { - // ground object - if (object.getMapSceneID() != -1) - { - blitMapDecoration(image, drawX, drawY, object); - } - } } } @@ -903,8 +950,13 @@ public class MapImageDumper for (Location location : region.getLocations()) { + int localX = location.getPosition().getX() - region.getBaseX(); + int localY = location.getPosition().getY() - region.getBaseY(); + boolean isBridge = (region.getTileSetting(1, localX, localY) & 2) != 0; + + int tileZ = z + (isBridge ? 1 : 0); int localZ = location.getPosition().getZ(); - if (z != 0 && localZ != z) + if (z != 0 && localZ != tileZ) { // draw all icons on z=0 continue; @@ -914,9 +966,6 @@ public class MapImageDumper assert od != null; - int localX = location.getPosition().getX() - region.getBaseX(); - int localY = location.getPosition().getY() - region.getBaseY(); - int drawX = drawBaseX + localX; int drawY = drawBaseY + (Region.Y - 1 - localY); From fa4776f3966b710d8cb302ceecb0611044d56a90 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sun, 17 Oct 2021 22:15:50 -0600 Subject: [PATCH 07/29] cache/MapImageDumper: fix object wall checks --- .../net/runelite/cache/MapImageDumper.java | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 6973dc86af..7c921b0138 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -596,15 +596,8 @@ public class MapImageDumper int drawX = (drawBaseX + localX) * MAP_SCALE; int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; - // this is a wall - int hash = (localY << 7) + localX + (location.getId() << 14) + 0x4000_0000; - if (object.getWallOrDoor() == 0) - { - hash -= Integer.MIN_VALUE; - } - int rgb = wallColor; - if (hash > 0) + if (object.getWallOrDoor() != 0) { rgb = doorColor; } @@ -721,19 +714,8 @@ public class MapImageDumper continue; } - int hash = (localY << 7) + localX + (location.getId() << 14) + 0x4000_0000; - if (object.getWallOrDoor() == 0) - { - hash -= Integer.MIN_VALUE; - } - - if ((hash >> 29 & 3) != 2) - { - continue; - } - int rgb = 0xFFEE_EEEE; - if (hash > 0) + if (object.getWallOrDoor() != 0) { rgb = 0xFFEE_0000; } @@ -765,7 +747,6 @@ public class MapImageDumper int drawX = (drawBaseX + localX) * MAP_SCALE; int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE; - // ground object if (object.getMapSceneID() != -1) { blitMapDecoration(image, drawX, drawY, object); From c04534b7cb5659813fccf953b92fb9614e49e443 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sun, 1 May 2022 16:34:07 -0600 Subject: [PATCH 08/29] cache/MapImageDumper: draw cross region objs when drawing single regions --- .../net/runelite/cache/MapImageDumper.java | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 7c921b0138..5a1a95c32d 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -181,6 +181,17 @@ public class MapImageDumper return image; } + private void drawNeighborObjects(BufferedImage image, int rx, int ry, int dx, int dy, int z) + { + Region neighbor = regionLoader.findRegionForRegionCoordinates(rx + dx, ry + dy); + if (neighbor == null) + { + return; + } + + drawObjects(image, Region.X * dx, Region.Y * -dy, neighbor, z); + } + public BufferedImage drawRegion(Region region, int z) { int pixelsX = Region.X * MAP_SCALE; @@ -189,7 +200,16 @@ public class MapImageDumper BufferedImage image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); drawMap(image, 0, 0, z, region); + + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), -1, -1, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), -1, 0, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), -1, 1, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 0, -1, z); drawObjects(image, 0, 0, region, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 0, 1, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 1, -1, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 1, 0, z); + drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 1, 1, z); drawMapIcons(image, 0, 0, region, z); return image; @@ -607,7 +627,7 @@ public class MapImageDumper { blitMapDecoration(image, drawX, drawY, object); } - else + else if (drawX >= 0 && drawY >= 0 && drawX < image.getWidth() && drawY < image.getHeight()) { if (type == 0 || type == 2) { @@ -714,25 +734,28 @@ public class MapImageDumper continue; } - int rgb = 0xFFEE_EEEE; - if (object.getWallOrDoor() != 0) + if (drawX >= 0 && drawY >= 0 && drawX < image.getWidth() && drawY < image.getHeight()) { - rgb = 0xFFEE_0000; - } + int rgb = 0xFFEE_EEEE; + if (object.getWallOrDoor() != 0) + { + rgb = 0xFFEE_0000; + } - if (rotation != 0 && rotation != 2) - { - image.setRGB(drawX + 0, drawY + 0, rgb); - image.setRGB(drawX + 1, drawY + 1, rgb); - image.setRGB(drawX + 2, drawY + 2, rgb); - image.setRGB(drawX + 3, drawY + 3, rgb); - } - else - { - image.setRGB(drawX + 0, drawY + 3, rgb); - image.setRGB(drawX + 1, drawY + 2, rgb); - image.setRGB(drawX + 2, drawY + 1, rgb); - image.setRGB(drawX + 3, drawY + 0, rgb); + if (rotation != 0 && rotation != 2) + { + image.setRGB(drawX + 0, drawY + 0, rgb); + image.setRGB(drawX + 1, drawY + 1, rgb); + image.setRGB(drawX + 2, drawY + 2, rgb); + image.setRGB(drawX + 3, drawY + 3, rgb); + } + else + { + image.setRGB(drawX + 0, drawY + 3, rgb); + image.setRGB(drawX + 1, drawY + 2, rgb); + image.setRGB(drawX + 2, drawY + 1, rgb); + image.setRGB(drawX + 3, drawY + 0, rgb); + } } } } From 75e23b9a3ec26053f427ee5fd0c81d89bf4b2086 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 1 May 2022 14:53:09 -0400 Subject: [PATCH 09/29] map image dumper: use BigBufferedImage This redudces the memory consumption significantly since it requires less of the image to be in memory at one time Co-authored-by: Explv --- .../net/runelite/cache/MapImageDumper.java | 15 +- .../runelite/cache/util/BigBufferedImage.java | 346 ++++++++++++++++++ 2 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 cache/src/main/java/net/runelite/cache/util/BigBufferedImage.java diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 5a1a95c32d..2c2e904064 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -57,6 +57,7 @@ import net.runelite.cache.region.Location; import net.runelite.cache.region.Position; import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; +import net.runelite.cache.util.BigBufferedImage; import net.runelite.cache.util.KeyProvider; @Slf4j @@ -110,6 +111,10 @@ public class MapImageDumper @Setter private boolean transparency = false; + @Getter + @Setter + private boolean lowMemory = true; + public MapImageDumper(Store store, KeyProvider keyProvider) { this(store, new RegionLoader(store, keyProvider)); @@ -172,7 +177,15 @@ public class MapImageDumper MAP_SCALE, (pixelsX * pixelsY * 3 / 1024 / 1024), Runtime.getRuntime().maxMemory() / 1024L / 1024L); - BufferedImage image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + BufferedImage image; + if (lowMemory) + { + image = BigBufferedImage.create(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + } + else + { + image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + } drawMap(image, z); drawObjects(image, z); diff --git a/cache/src/main/java/net/runelite/cache/util/BigBufferedImage.java b/cache/src/main/java/net/runelite/cache/util/BigBufferedImage.java new file mode 100644 index 0000000000..2d7612174a --- /dev/null +++ b/cache/src/main/java/net/runelite/cache/util/BigBufferedImage.java @@ -0,0 +1,346 @@ +package net.runelite.cache.util; + +/* + * This class is part of MCFS (Mission Control - Flight Software) a development + * of Team Puli Space, official Google Lunar XPRIZE contestant. + * This class is released under Creative Commons CC0. + * @author Zsolt Pocze, Dimitry Polivaev + * Please like us on facebook, and/or join our Small Step Club. + * http://www.pulispace.com + * https://www.facebook.com/pulispace + * http://nyomdmegteis.hu/en/ + */ + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.color.ColorSpace; +import java.awt.image.BandedSampleModel; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BigBufferedImage extends BufferedImage +{ + private static final String TMP_DIR = System.getProperty("java.io.tmpdir"); + private static final int MAX_PIXELS_IN_MEMORY = 1024 * 1024; + + public static BufferedImage create(int width, int height, int imageType) + { + if (width * height > MAX_PIXELS_IN_MEMORY) + { + try + { + final File tempDir = new File(TMP_DIR); + return createBigBufferedImage(tempDir, width, height, imageType); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + else + { + return new BufferedImage(width, height, imageType); + } + } + + public static BufferedImage create(File inputFile, int imageType) throws IOException + { + try (ImageInputStream stream = ImageIO.createImageInputStream(inputFile)) + { + Iterator readers = ImageIO.getImageReaders(stream); + if (readers.hasNext()) + { + try + { + ImageReader reader = readers.next(); + reader.setInput(stream, true, true); + int width = reader.getWidth(reader.getMinIndex()); + int height = reader.getHeight(reader.getMinIndex()); + BufferedImage image = create(width, height, imageType); + int cores = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); + int block = Math.min(MAX_PIXELS_IN_MEMORY / cores / width, (int) (Math.ceil(height / (double) cores))); + ExecutorService generalExecutor = Executors.newFixedThreadPool(cores); + List> partLoaders = new ArrayList<>(); + for (int y = 0; y < height; y += block) + { + partLoaders.add(new ImagePartLoader( + y, width, Math.min(block, height - y), inputFile, image)); + } + generalExecutor.invokeAll(partLoaders); + generalExecutor.shutdown(); + return image; + } + catch (InterruptedException ex) + { + log.error(null, ex); + } + } + } + return null; + } + + private static BufferedImage createBigBufferedImage(File tempDir, int width, int height, int imageType) + throws IOException + { + FileDataBuffer buffer = new FileDataBuffer(tempDir, width * height, 4); + ColorModel colorModel; + BandedSampleModel sampleModel; + switch (imageType) + { + case TYPE_INT_RGB: + colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[]{8, 8, 8, 0}, + false, + false, + ComponentColorModel.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 3); + break; + case TYPE_INT_ARGB: + colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[]{8, 8, 8, 8}, + true, + false, + ComponentColorModel.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + sampleModel = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, 4); + break; + default: + throw new IllegalArgumentException("Unsupported image type: " + imageType); + } + SimpleRaster raster = new SimpleRaster(sampleModel, buffer, new Point(0, 0)); + BigBufferedImage image = new BigBufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null); + return image; + } + + private static class ImagePartLoader implements Callable + { + private final int y; + private final BufferedImage image; + private final Rectangle region; + private final File file; + + public ImagePartLoader(int y, int width, int height, File file, BufferedImage image) + { + this.y = y; + this.image = image; + this.file = file; + region = new Rectangle(0, y, width, height); + } + + @Override + public ImagePartLoader call() throws Exception + { + Thread.currentThread().setPriority((Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2); + try (ImageInputStream stream = ImageIO.createImageInputStream(file);) + { + Iterator readers = ImageIO.getImageReaders(stream); + if (readers.hasNext()) + { + ImageReader reader = readers.next(); + reader.setInput(stream, true, true); + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(region); + BufferedImage part = reader.read(0, param); + Raster source = part.getRaster(); + WritableRaster target = image.getRaster(); + target.setRect(0, y, source); + } + } + return ImagePartLoader.this; + } + } + + private BigBufferedImage(ColorModel cm, SimpleRaster raster, boolean isRasterPremultiplied, Hashtable properties) + { + super(cm, raster, isRasterPremultiplied, properties); + } + + public void dispose() + { + ((SimpleRaster) getRaster()).dispose(); + } + + public static void dispose(RenderedImage image) + { + if (image instanceof BigBufferedImage) + { + ((BigBufferedImage) image).dispose(); + } + } + + private static class SimpleRaster extends WritableRaster + { + public SimpleRaster(SampleModel sampleModel, FileDataBuffer dataBuffer, Point origin) + { + super(sampleModel, dataBuffer, origin); + } + + public void dispose() + { + ((FileDataBuffer) getDataBuffer()).dispose(); + } + } + + private static final class FileDataBufferDeleterHook extends Thread + { + static + { + Runtime.getRuntime().addShutdownHook(new FileDataBufferDeleterHook()); + } + + private static final HashSet undisposedBuffers = new HashSet<>(); + + @Override + public void run() + { + final FileDataBuffer[] buffers = undisposedBuffers.toArray(new FileDataBuffer[0]); + for (FileDataBuffer b : buffers) + { + b.disposeNow(); + } + } + } + + private static class FileDataBuffer extends DataBuffer + { + private final String id = "buffer-" + System.currentTimeMillis() + "-" + ((int) (Math.random() * 1000)); + private File dir; + private String path; + private File[] files; + private RandomAccessFile[] accessFiles; + private MappedByteBuffer[] buffer; + + public FileDataBuffer(File dir, int size) throws IOException + { + super(TYPE_BYTE, size); + this.dir = dir; + init(); + } + + public FileDataBuffer(File dir, int size, int numBanks) throws IOException + { + super(TYPE_BYTE, size, numBanks); + this.dir = dir; + init(); + } + + private void init() throws IOException + { + FileDataBufferDeleterHook.undisposedBuffers.add(this); + if (dir == null) + { + dir = new File("."); + } + if (!dir.exists()) + { + throw new RuntimeException("FileDataBuffer constructor parameter dir does not exist: " + dir); + } + if (!dir.isDirectory()) + { + throw new RuntimeException("FileDataBuffer constructor parameter dir is not a directory: " + dir); + } + path = dir.getPath() + "/" + id; + File subDir = new File(path); + subDir.mkdir(); + buffer = new MappedByteBuffer[banks]; + accessFiles = new RandomAccessFile[banks]; + files = new File[banks]; + for (int i = 0; i < banks; i++) + { + File file = files[i] = new File(path + "/bank" + i + ".dat"); + final RandomAccessFile randomAccessFile = accessFiles[i] = new RandomAccessFile(file, "rw"); + buffer[i] = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, getSize()); + } + } + + @Override + public int getElem(int bank, int i) + { + return buffer[bank].get(i) & 0xff; + } + + @Override + public void setElem(int bank, int i, int val) + { + buffer[bank].put(i, (byte) val); + } + + @Override + protected void finalize() throws Throwable + { + dispose(); + } + + public void dispose() + { + new Thread() + { + @Override + public void run() + { + disposeNow(); + } + }.start(); + } + + private void disposeNow() + { + this.buffer = null; + if (accessFiles != null) + { + for (RandomAccessFile file : accessFiles) + { + try + { + file.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + accessFiles = null; + } + if (files != null) + { + for (File file : files) + { + file.delete(); + } + files = null; + } + if (path != null) + { + new File(path).delete(); + path = null; + } + } + + } +} \ No newline at end of file From 54da56e7b142fcd5317497bec5344ff5ec0e2e6a Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 1 May 2022 14:53:12 -0400 Subject: [PATCH 10/29] map image dumper: add main method Co-authored-by: Explv --- .../net/runelite/cache/MapImageDumper.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/cache/src/main/java/net/runelite/cache/MapImageDumper.java b/cache/src/main/java/net/runelite/cache/MapImageDumper.java index 2c2e904064..83b7d5a426 100644 --- a/cache/src/main/java/net/runelite/cache/MapImageDumper.java +++ b/cache/src/main/java/net/runelite/cache/MapImageDumper.java @@ -27,12 +27,15 @@ package net.runelite.cache; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.imageio.ImageIO; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @@ -59,6 +62,13 @@ import net.runelite.cache.region.Region; import net.runelite.cache.region.RegionLoader; import net.runelite.cache.util.BigBufferedImage; import net.runelite.cache.util.KeyProvider; +import net.runelite.cache.util.XteaKeyManager; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; @Slf4j @Accessors(chain = true) @@ -129,6 +139,59 @@ public class MapImageDumper this.objectManager = new ObjectManager(store); } + public static void main(String[] args) throws IOException + { + Options options = new Options(); + options.addOption(Option.builder().longOpt("cachedir").hasArg().required().build()); + options.addOption(Option.builder().longOpt("xteapath").hasArg().required().build()); + options.addOption(Option.builder().longOpt("outputdir").hasArg().required().build()); + + CommandLineParser parser = new DefaultParser(); + CommandLine cmd; + try + { + cmd = parser.parse(options, args); + } + catch (ParseException ex) + { + System.err.println("Error parsing command line options: " + ex.getMessage()); + System.exit(-1); + return; + } + + final String cacheDirectory = cmd.getOptionValue("cachedir"); + final String xteaJSONPath = cmd.getOptionValue("xteapath"); + final String outputDirectory = cmd.getOptionValue("outputdir"); + + XteaKeyManager xteaKeyManager = new XteaKeyManager(); + try (FileInputStream fin = new FileInputStream(xteaJSONPath)) + { + xteaKeyManager.loadKeys(fin); + } + + File base = new File(cacheDirectory); + File outDir = new File(outputDirectory); + outDir.mkdirs(); + + try (Store store = new Store(base)) + { + store.load(); + + MapImageDumper dumper = new MapImageDumper(store, xteaKeyManager); + dumper.load(); + + for (int i = 0; i < Region.Z; ++i) + { + BufferedImage image = dumper.drawMap(i); + + File imageFile = new File(outDir, "img-" + i + ".png"); + + ImageIO.write(image, "png", imageFile); + log.info("Wrote image {}", imageFile); + } + } + } + protected double random() { // the client would use a random value here, but we prefer determinism From a5514641800be3ec3b5984f8e6175693a4832084 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 1 May 2022 15:06:01 -0400 Subject: [PATCH 11/29] cache: make slf4j-simple an optional dependency --- cache/pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cache/pom.xml b/cache/pom.xml index 3cae4b3fe8..abdf2ea434 100644 --- a/cache/pom.xml +++ b/cache/pom.xml @@ -50,6 +50,11 @@ org.slf4j slf4j-api + + org.slf4j + slf4j-simple + true + org.apache.commons commons-compress @@ -81,11 +86,6 @@ 4.12 test - - org.slf4j - slf4j-simple - test - net.runelite.rs cache From d110a3c28e00caf7cb4168009fe8ee2ccc425c4d Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 5 May 2022 19:19:55 -0400 Subject: [PATCH 12/29] api: add object composition map setters --- .../java/net/runelite/api/ObjectComposition.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/runelite-api/src/main/java/net/runelite/api/ObjectComposition.java b/runelite-api/src/main/java/net/runelite/api/ObjectComposition.java index 1aa69b6082..1587022906 100644 --- a/runelite-api/src/main/java/net/runelite/api/ObjectComposition.java +++ b/runelite-api/src/main/java/net/runelite/api/ObjectComposition.java @@ -56,12 +56,26 @@ public interface ObjectComposition extends ParamHolder */ int getMapSceneId(); + /** + * Set the map scene index into the {@link Client#getMapScene()} + * array, or -1 if it has no map scene icon + * @param mapSceneId + */ + void setMapSceneId(int mapSceneId); + /** * Gets the index of this object in the {@link Client#getMapIcons()} * array, or -1 if it has no full map icon */ int getMapIconId(); + /** + * Set the index of the object in the {@link Client#getMapIcons()} + * array, or -1 if it has no map icon + * @param mapIconId + */ + void setMapIconId(int mapIconId); + /** * Get the {@link ObjectID}s of objects this can transform into, depending * on a {@link Varbits} or {@link VarPlayer} From 230db67f07e14a1a4cad8ebef903fb59efcf854b Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 5 May 2022 19:30:24 -0400 Subject: [PATCH 13/29] api: add post object composition event --- .../api/events/PostItemComposition.java | 4 +- .../api/events/PostObjectComposition.java | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 runelite-api/src/main/java/net/runelite/api/events/PostObjectComposition.java diff --git a/runelite-api/src/main/java/net/runelite/api/events/PostItemComposition.java b/runelite-api/src/main/java/net/runelite/api/events/PostItemComposition.java index 1e9000cfd1..ec69b90480 100644 --- a/runelite-api/src/main/java/net/runelite/api/events/PostItemComposition.java +++ b/runelite-api/src/main/java/net/runelite/api/events/PostItemComposition.java @@ -24,14 +24,14 @@ */ package net.runelite.api.events; -import lombok.Data; +import lombok.Value; import net.runelite.api.ItemComposition; /** * An event called after a new {@link ItemComposition} is created and * its data is initialized. */ -@Data +@Value public class PostItemComposition { /** diff --git a/runelite-api/src/main/java/net/runelite/api/events/PostObjectComposition.java b/runelite-api/src/main/java/net/runelite/api/events/PostObjectComposition.java new file mode 100644 index 0000000000..820909f158 --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/events/PostObjectComposition.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022, Adam + * 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.api.events; + +import lombok.Value; +import net.runelite.api.ObjectComposition; + +/** + * An event called after a new {@link ObjectComposition} is created and + * its data is initialized. + */ +@Value +public class PostObjectComposition +{ + /** + * The newly created object + */ + ObjectComposition objectComposition; +} From d6260f247fd1b6010340eb67b265edbf50a92fcb Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 5 May 2022 19:42:08 -0400 Subject: [PATCH 14/29] api: add object composition cache accessor --- runelite-api/src/main/java/net/runelite/api/Client.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 fb547e0284..b38a78d338 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -1920,6 +1920,12 @@ public interface Client extends OAuthApi, GameEngine */ NodeCache getItemCompositionCache(); + /** + * Returns client object composition cache + * @return + */ + NodeCache getObjectCompositionCache(); + /** * Returns the array of cross sprites that appear and animate when left-clicking */ From 162a2f78cb7be953ee2dd4319413a388211235fc Mon Sep 17 00:00:00 2001 From: Bob Heine Date: Fri, 6 May 2022 10:27:38 -0500 Subject: [PATCH 15/29] clues: Add abyssal lanterns as light sources (#14923) --- .../plugins/cluescrolls/ClueScrollOverlay.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java index b4d21959c2..3f3e5d8162 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java @@ -70,7 +70,20 @@ public class ClueScrollOverlay extends OverlayPanel item(KANDARIN_HEADGEAR_4), item(BRUMA_TORCH), item(MAX_CAPE), - item(MAX_CAPE_13342)); + item(MAX_CAPE_13342), + item(ABYSSAL_LANTERN_NORMAL_LOGS), + item(ABYSSAL_LANTERN_BLUE_LOGS), + item(ABYSSAL_LANTERN_RED_LOGS), + item(ABYSSAL_LANTERN_WHITE_LOGS), + item(ABYSSAL_LANTERN_PURPLE_LOGS), + item(ABYSSAL_LANTERN_GREEN_LOGS), + item(ABYSSAL_LANTERN_OAK_LOGS), + item(ABYSSAL_LANTERN_WILLOW_LOGS), + item(ABYSSAL_LANTERN_MAPLE_LOGS), + item(ABYSSAL_LANTERN_YEW_LOGS), + item(ABYSSAL_LANTERN_BLISTERWOOD_LOGS), + item(ABYSSAL_LANTERN_MAGIC_LOGS), + item(ABYSSAL_LANTERN_REDWOOD_LOGS)); public static final Color TITLED_CONTENT_COLOR = new Color(190, 190, 190); From b5515075efa35319ede070c3c4f49dcca085c859 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 6 May 2022 14:50:10 -0400 Subject: [PATCH 16/29] chatfilter: add config option for stripping accents --- .../client/plugins/chatfilter/ChatFilterConfig.java | 11 +++++++++++ .../client/plugins/chatfilter/ChatFilterPlugin.java | 13 +++++++++---- .../plugins/chatfilter/ChatFilterPluginTest.java | 2 ++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java index e4a7c06ca9..1cc6fcb715 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterConfig.java @@ -164,4 +164,15 @@ public interface ChatFilterConfig extends Config { return 0; } + + @ConfigItem( + keyName = "stripAccents", + name = "Strip accents", + description = "Remove accents before applying filters", + position = 13 + ) + default boolean stripAccents() + { + return false; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java index a529800017..e5befde35f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatfilter/ChatFilterPlugin.java @@ -321,7 +321,7 @@ public class ChatFilterPlugin extends Plugin { String strippedMessage = jagexPrintableCharMatcher.retainFrom(message) .replace('\u00A0', ' '); - String strippedAccents = StringUtils.stripAccents(strippedMessage); + String strippedAccents = stripAccents(strippedMessage); assert strippedMessage.length() == strippedAccents.length(); if (username != null && shouldFilterByName(username)) @@ -377,23 +377,28 @@ public class ChatFilterPlugin extends Plugin filteredNamePatterns.clear(); Text.fromCSV(config.filteredWords()).stream() - .map(StringUtils::stripAccents) + .map(this::stripAccents) .map(s -> Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE)) .forEach(filteredPatterns::add); NEWLINE_SPLITTER.splitToList(config.filteredRegex()).stream() - .map(StringUtils::stripAccents) + .map(this::stripAccents) .map(ChatFilterPlugin::compilePattern) .filter(Objects::nonNull) .forEach(filteredPatterns::add); NEWLINE_SPLITTER.splitToList(config.filteredNames()).stream() - .map(StringUtils::stripAccents) + .map(this::stripAccents) .map(ChatFilterPlugin::compilePattern) .filter(Objects::nonNull) .forEach(filteredNamePatterns::add); } + private String stripAccents(String input) + { + return config.stripAccents() ? StringUtils.stripAccents(input) : input; + } + private static Pattern compilePattern(String pattern) { try diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java index b93f3b1078..324e3dacdf 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/chatfilter/ChatFilterPluginTest.java @@ -191,6 +191,7 @@ public class ChatFilterPluginTest { when(chatFilterConfig.filterType()).thenReturn(ChatFilterType.CENSOR_WORDS); when(chatFilterConfig.filteredWords()).thenReturn("filterme"); + when(chatFilterConfig.stripAccents()).thenReturn(true); chatFilterPlugin.updateFilteredPatterns(); assertEquals("plëäsë ******** plügïn", chatFilterPlugin.censorMessage("Blue", "plëäsë fïltërmë plügïn")); @@ -211,6 +212,7 @@ public class ChatFilterPluginTest { when(chatFilterConfig.filterType()).thenReturn(ChatFilterType.CENSOR_WORDS); when(chatFilterConfig.filteredWords()).thenReturn("plëäsë, filterme"); + when(chatFilterConfig.stripAccents()).thenReturn(true); chatFilterPlugin.updateFilteredPatterns(); assertEquals("****** ******** plügïn", chatFilterPlugin.censorMessage("Blue", "plëäsë fïltërmë plügïn")); From 2fa6eb1dc38f4df36d2b063027d2469b683a7dfb Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 7 May 2022 09:38:19 -0400 Subject: [PATCH 17/29] cache: update indextype names --- .../main/java/net/runelite/cache/IndexType.java | 17 ++++++++++------- .../java/net/runelite/cache/FrameDumper.java | 4 ++-- .../java/net/runelite/cache/FramemapDumper.java | 2 +- .../net/runelite/cache/TrackDumperTest.java | 4 ++-- .../net/runelite/cache/WorldMapDumperTest.java | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cache/src/main/java/net/runelite/cache/IndexType.java b/cache/src/main/java/net/runelite/cache/IndexType.java index 813594bac5..a7f75342b1 100644 --- a/cache/src/main/java/net/runelite/cache/IndexType.java +++ b/cache/src/main/java/net/runelite/cache/IndexType.java @@ -27,23 +27,26 @@ package net.runelite.cache; public enum IndexType { - FRAMES(0), - FRAMEMAPS(1), + ANIMATIONS(0), + SKELETONS(1), CONFIGS(2), INTERFACES(3), SOUNDEFFECTS(4), MAPS(5), - TRACK1(6), + MUSIC_TRACKS(6), MODELS(7), SPRITES(8), TEXTURES(9), BINARY(10), - TRACK2(11), + MUSIC_JINGLES(11), CLIENTSCRIPT(12), FONTS(13), - VORBIS(14), - INSTRUMENTS(15), - WORLDMAP(16); + MUSIC_SAMPLES(14), + MUSIC_PATCHES(15), + WORLDMAP_OLD(16), // looks unused + WORLDMAP_GEOGRAPHY(18), + WORLDMAP(19), + WORLDMAP_GROUND(20); private int id; diff --git a/cache/src/test/java/net/runelite/cache/FrameDumper.java b/cache/src/test/java/net/runelite/cache/FrameDumper.java index f6cc20532d..3619d7e163 100644 --- a/cache/src/test/java/net/runelite/cache/FrameDumper.java +++ b/cache/src/test/java/net/runelite/cache/FrameDumper.java @@ -72,8 +72,8 @@ public class FrameDumper store.load(); Storage storage = store.getStorage(); - Index frameIndex = store.getIndex(IndexType.FRAMES); - Index framemapIndex = store.getIndex(IndexType.FRAMEMAPS); + Index frameIndex = store.getIndex(IndexType.ANIMATIONS); + Index framemapIndex = store.getIndex(IndexType.SKELETONS); for (Archive archive : frameIndex.getArchives()) { diff --git a/cache/src/test/java/net/runelite/cache/FramemapDumper.java b/cache/src/test/java/net/runelite/cache/FramemapDumper.java index 8feef1f814..ea44ae7d14 100644 --- a/cache/src/test/java/net/runelite/cache/FramemapDumper.java +++ b/cache/src/test/java/net/runelite/cache/FramemapDumper.java @@ -66,7 +66,7 @@ public class FramemapDumper store.load(); Storage storage = store.getStorage(); - Index index = store.getIndex(IndexType.FRAMEMAPS); + Index index = store.getIndex(IndexType.SKELETONS); for (Archive archive : index.getArchives()) { diff --git a/cache/src/test/java/net/runelite/cache/TrackDumperTest.java b/cache/src/test/java/net/runelite/cache/TrackDumperTest.java index dc70a798ef..0a7079ab7d 100644 --- a/cache/src/test/java/net/runelite/cache/TrackDumperTest.java +++ b/cache/src/test/java/net/runelite/cache/TrackDumperTest.java @@ -67,8 +67,8 @@ public class TrackDumperTest store.load(); Storage storage = store.getStorage(); - Index index = store.getIndex(IndexType.TRACK1); - Index index2 = store.getIndex(IndexType.TRACK2); + Index index = store.getIndex(IndexType.MUSIC_TRACKS); + Index index2 = store.getIndex(IndexType.MUSIC_JINGLES); for (Archive archive : index.getArchives()) { diff --git a/cache/src/test/java/net/runelite/cache/WorldMapDumperTest.java b/cache/src/test/java/net/runelite/cache/WorldMapDumperTest.java index 92b2582a91..1f3a8607af 100644 --- a/cache/src/test/java/net/runelite/cache/WorldMapDumperTest.java +++ b/cache/src/test/java/net/runelite/cache/WorldMapDumperTest.java @@ -66,7 +66,7 @@ public class WorldMapDumperTest store.load(); Storage storage = store.getStorage(); - Index index = store.getIndex(IndexType.WORLDMAP); + Index index = store.getIndex(IndexType.WORLDMAP_OLD); Archive archive = index.getArchive(0); // there is also archive 1/2, but their data format is not this byte[] archiveData = storage.loadArchive(archive); From 026395fad5dcf15b6166ae71b271a5606a031ec1 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 6 May 2022 21:27:14 -0400 Subject: [PATCH 18/29] chat commands: fix loading pets modicons The asyncbufferedimage may not be loaded at the time the modicons is setup, so a listener must be added to assign the final image --- .../chatcommands/ChatCommandsPlugin.java | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java index e918593416..67359d10ea 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java @@ -90,6 +90,7 @@ import net.runelite.client.hiscore.Skill; import net.runelite.client.input.KeyManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.util.AsyncBufferedImage; import net.runelite.client.util.ImageUtil; import net.runelite.client.util.QuantityFormatter; import net.runelite.client.util.Text; @@ -220,7 +221,15 @@ public class ChatCommandsPlugin extends Plugin chatCommandManager.registerCommandAsync(SOUL_WARS_ZEAL_COMMAND, this::soulWarsZealLookup); chatCommandManager.registerCommandAsync(PET_LIST_COMMAND, this::petListLookup, this::petListSubmit); - clientThread.invoke(this::loadPetIcons); + clientThread.invoke(() -> + { + if (client.getModIcons() == null) + { + return false; + } + loadPetIcons(); + return true; + }); } @Override @@ -295,13 +304,14 @@ public class ChatCommandsPlugin extends Plugin private void loadPetIcons() { - final IndexedSprite[] modIcons = client.getModIcons(); - if (modIconIdx != -1 || modIcons == null) + if (modIconIdx != -1) { return; } final Pet[] pets = Pet.values(); + final IndexedSprite[] modIcons = client.getModIcons(); + assert modIcons != null; final IndexedSprite[] newModIcons = Arrays.copyOf(modIcons, modIcons.length + pets.length); modIconIdx = modIcons.length; @@ -309,9 +319,16 @@ public class ChatCommandsPlugin extends Plugin { final Pet pet = pets[i]; - final BufferedImage image = ImageUtil.resizeImage(itemManager.getImage(pet.getIconID()), 18, 16); - final IndexedSprite sprite = ImageUtil.getImageIndexedSprite(image, client); - newModIcons[modIconIdx + i] = sprite; + final AsyncBufferedImage abi = itemManager.getImage(pet.getIconID()); + final int idx = modIconIdx + i; + Runnable r = () -> + { + final BufferedImage image = ImageUtil.resizeImage(abi, 18, 16); + final IndexedSprite sprite = ImageUtil.getImageIndexedSprite(image, client); + newModIcons[idx] = sprite; + }; + abi.onLoaded(r); + r.run(); } client.setModIcons(newModIcons); @@ -784,9 +801,6 @@ public class ChatCommandsPlugin extends Plugin case HOPPING: pohOwner = null; break; - case LOGGED_IN: - loadPetIcons(); - break; } } From 44de746d5bfe0066c9d8f38efdf044d031c5ff43 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 6 May 2022 21:45:39 -0400 Subject: [PATCH 19/29] emoji: clean up modicons loading This just needs to be run after modicons is initialized, and doesn't need to be checked on gamestate change. --- .../client/plugins/emojis/EmojiPlugin.java | 21 +++++----- .../plugins/emojis/EmojiPluginTest.java | 42 ++++++++++--------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/emojis/EmojiPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/emojis/EmojiPlugin.java index 39d022c3e7..7b72f1483d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/emojis/EmojiPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/emojis/EmojiPlugin.java @@ -37,7 +37,6 @@ import net.runelite.api.IndexedSprite; import net.runelite.api.MessageNode; import net.runelite.api.Player; import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.OverheadTextChanged; import net.runelite.client.callback.ClientThread; import net.runelite.client.eventbus.Subscribe; @@ -67,27 +66,27 @@ public class EmojiPlugin extends Plugin @Override protected void startUp() { - clientThread.invoke(this::loadEmojiIcons); - } - - @Subscribe - public void onGameStateChanged(GameStateChanged gameStateChanged) - { - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) + clientThread.invoke(() -> { + if (client.getModIcons() == null) + { + return false; + } loadEmojiIcons(); - } + return true; + }); } private void loadEmojiIcons() { - final IndexedSprite[] modIcons = client.getModIcons(); - if (modIconsStart != -1 || modIcons == null) + if (modIconsStart != -1) { return; } final Emoji[] emojis = Emoji.values(); + final IndexedSprite[] modIcons = client.getModIcons(); + assert modIcons != null; final IndexedSprite[] newModIcons = Arrays.copyOf(modIcons, modIcons.length + emojis.length); modIconsStart = modIcons.length; diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/emojis/EmojiPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/emojis/EmojiPluginTest.java index 99fc2dc174..3157b15575 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/emojis/EmojiPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/emojis/EmojiPluginTest.java @@ -28,24 +28,27 @@ import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.util.function.BooleanSupplier; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.IndexedSprite; import net.runelite.api.MessageNode; import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameStateChanged; +import net.runelite.client.callback.ClientThread; import net.runelite.client.chat.ChatMessageManager; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; import org.mockito.Mock; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.mockito.junit.MockitoJUnitRunner; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; @RunWith(MockitoJUnitRunner.class) public class EmojiPluginTest @@ -58,6 +61,10 @@ public class EmojiPluginTest @Bind private ChatMessageManager chatMessageManager; + @Mock + @Bind + private ClientThread clientThread; + @Inject private EmojiPlugin emojiPlugin; @@ -65,19 +72,23 @@ public class EmojiPluginTest public void before() { Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + + when(client.getModIcons()).thenReturn(new IndexedSprite[0]); + when(client.createIndexedSprite()).thenReturn(mock(IndexedSprite.class)); + + doAnswer(a -> + { + final BooleanSupplier b = a.getArgument(0); + return b.getAsBoolean(); + }).when(clientThread).invoke(any(BooleanSupplier.class)); + + emojiPlugin.startUp(); } @Test public void testOnChatMessage() { when(client.getGameState()).thenReturn(GameState.LOGGED_IN); - when(client.getModIcons()).thenReturn(new IndexedSprite[0]); - when(client.createIndexedSprite()).thenReturn(mock(IndexedSprite.class)); - - // Trip emoji loading - GameStateChanged gameStateChanged = new GameStateChanged(); - gameStateChanged.setGameState(GameState.LOGGED_IN); - emojiPlugin.onGameStateChanged(gameStateChanged); MessageNode messageNode = mock(MessageNode.class); // With chat recolor, message may be wrapped in col tags @@ -96,13 +107,6 @@ public class EmojiPluginTest public void testGtLt() { when(client.getGameState()).thenReturn(GameState.LOGGED_IN); - when(client.getModIcons()).thenReturn(new IndexedSprite[0]); - when(client.createIndexedSprite()).thenReturn(mock(IndexedSprite.class)); - - // Trip emoji loading - GameStateChanged gameStateChanged = new GameStateChanged(); - gameStateChanged.setGameState(GameState.LOGGED_IN); - emojiPlugin.onGameStateChanged(gameStateChanged); MessageNode messageNode = mock(MessageNode.class); when(messageNode.getValue()).thenReturn(":D"); @@ -119,8 +123,8 @@ public class EmojiPluginTest @Test public void testEmojiUpdateMessage() { - String PARTY_POPPER = "'; - String OPEN_MOUTH = "'; + String PARTY_POPPER = "'; + String OPEN_MOUTH = "'; assertNull(emojiPlugin.updateMessage("@@@@@")); assertEquals(PARTY_POPPER, emojiPlugin.updateMessage("@@@")); assertEquals(PARTY_POPPER + ' ' + PARTY_POPPER, emojiPlugin.updateMessage("@@@ @@@")); From bb1f49b0b46ad3d070d6ccadd1198af6feeb6431 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 6 May 2022 21:45:40 -0400 Subject: [PATCH 20/29] friendnotes: clean up modicons loading This just needs to be run after modicons is initialized, and doesn't need to be checked on gamestate change. --- .../friendnotes/FriendNotesPlugin.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java index c274b74834..b8032f9e1e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java @@ -44,7 +44,6 @@ import net.runelite.api.IndexedSprite; import net.runelite.api.MenuAction; import net.runelite.api.Nameable; import net.runelite.api.ScriptID; -import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.events.NameableNameChanged; import net.runelite.api.events.RemovedFriend; @@ -116,7 +115,15 @@ public class FriendNotesPlugin extends Plugin protected void startUp() throws Exception { overlayManager.add(overlay); - clientThread.invoke(this::loadIcon); + clientThread.invoke(() -> + { + if (client.getModIcons() == null) + { + return false; + } + loadIcon(); + return true; + }); if (client.getGameState() == GameState.LOGGED_IN) { rebuildFriendsList(); @@ -135,15 +142,6 @@ public class FriendNotesPlugin extends Plugin } } - @Subscribe - public void onGameStateChanged(GameStateChanged event) - { - if (event.getGameState() == GameState.LOGGED_IN) - { - loadIcon(); - } - } - @Subscribe public void onConfigChanged(ConfigChanged event) { @@ -380,8 +378,7 @@ public class FriendNotesPlugin extends Plugin private void loadIcon() { - final IndexedSprite[] modIcons = client.getModIcons(); - if (iconIdx != -1 || modIcons == null) + if (iconIdx != -1) { return; } @@ -394,6 +391,8 @@ public class FriendNotesPlugin extends Plugin final BufferedImage resized = ImageUtil.resizeImage(iconImg, ICON_WIDTH, ICON_HEIGHT); + final IndexedSprite[] modIcons = client.getModIcons(); + assert modIcons != null; final IndexedSprite[] newIcons = Arrays.copyOf(modIcons, modIcons.length + 1); newIcons[newIcons.length - 1] = ImageUtil.getImageIndexedSprite(resized, client); From 0500f9483024a823a24597326cd7028d5705a8a9 Mon Sep 17 00:00:00 2001 From: LlemonDuck Date: Sat, 7 May 2022 14:19:13 -0700 Subject: [PATCH 21/29] timetracking: add compost tracking --- .../net/runelite/api/widgets/WidgetID.java | 1 + .../net/runelite/api/widgets/WidgetInfo.java | 1 + .../timetracking/TimeTrackingConfig.java | 1 + .../timetracking/TimeTrackingPlugin.java | 12 + .../plugins/timetracking/TimeablePanel.java | 41 ++- .../timetracking/farming/CompostState.java | 43 +++ .../timetracking/farming/CompostTracker.java | 293 ++++++++++++++++ .../timetracking/farming/FarmingPatch.java | 5 + .../timetracking/farming/FarmingTabPanel.java | 27 +- .../timetracking/farming/FarmingTracker.java | 20 +- .../farming/CompostTrackerTest.java | 323 ++++++++++++++++++ 11 files changed, 758 insertions(+), 9 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostState.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostTracker.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/CompostTrackerTest.java diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java index 3f1261d1d3..27b3f9054b 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java @@ -773,6 +773,7 @@ public final class WidgetID static class LunarSpellBook { static final int LUNAR_HOME_TELEPORT = 101; + static final int FERTILE_SOIL = 126; } static class ArceuusSpellBook diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java index c85a5341c9..12866d2110 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java @@ -505,6 +505,7 @@ public enum WidgetInfo SPELL_ARCEUUS_HOME_TELEPORT(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.ArceuusSpellBook.ARCEUUS_HOME_TELEPORT), SPELL_KOUREND_HOME_TELEPORT(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.StandardSpellBook.KOUREND_HOME_TELEPORT), SPELL_CATHERBY_HOME_TELEPORT(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.StandardSpellBook.CATHERBY_HOME_TELEPORT), + SPELL_LUNAR_FERTILE_SOIL(WidgetID.SPELLBOOK_GROUP_ID, WidgetID.LunarSpellBook.FERTILE_SOIL), PVP_WILDERNESS_SKULL_CONTAINER(WidgetID.PVP_GROUP_ID, WidgetID.Pvp.WILDERNESS_SKULL_CONTAINER), PVP_SKULL_CONTAINER(WidgetID.PVP_GROUP_ID, WidgetID.Pvp.SKULL_CONTAINER), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java index 8b6e7b2d58..36493a0373 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingConfig.java @@ -43,6 +43,7 @@ public interface TimeTrackingConfig extends Config String PREFER_SOONEST = "preferSoonest"; String NOTIFY = "notify"; String BIRDHOUSE_NOTIFY = "birdHouseNotification"; + String COMPOST = "compost"; @ConfigItem( keyName = "timeFormatMode", diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java index ca6df40a4d..6378193a06 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeTrackingPlugin.java @@ -44,6 +44,7 @@ import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.api.widgets.WidgetModalMode; import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.RuneScapeProfileChanged; @@ -54,6 +55,7 @@ import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.PREFER import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.STOPWATCHES; import static net.runelite.client.plugins.timetracking.TimeTrackingConfig.TIMERS; import net.runelite.client.plugins.timetracking.clocks.ClockManager; +import net.runelite.client.plugins.timetracking.farming.CompostTracker; import net.runelite.client.plugins.timetracking.farming.FarmingContractManager; import net.runelite.client.plugins.timetracking.farming.FarmingTracker; import net.runelite.client.plugins.timetracking.hunter.BirdHouseTracker; @@ -77,6 +79,12 @@ public class TimeTrackingPlugin extends Plugin @Inject private Client client; + @Inject + private EventBus eventBus; + + @Inject + private CompostTracker compostTracker; + @Inject private FarmingTracker farmingTracker; @@ -125,6 +133,8 @@ public class TimeTrackingPlugin extends Plugin birdHouseTracker.loadFromConfig(); farmingTracker.loadCompletionTimes(); + eventBus.register(compostTracker); + final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "watch.png"); panel = injector.getInstance(TimeTrackingPanel.class); @@ -148,6 +158,8 @@ public class TimeTrackingPlugin extends Plugin lastTickLocation = null; lastTickPostLogin = false; + eventBus.unregister(compostTracker); + if (panelUpdateFuture != null) { panelUpdateFuture.cancel(true); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java index 8903a0b715..dfdda1d750 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/TimeablePanel.java @@ -29,8 +29,11 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridLayout; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; import javax.swing.ImageIcon; import javax.swing.JLabel; +import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.JToggleButton; import javax.swing.border.EmptyBorder; @@ -48,9 +51,20 @@ public class TimeablePanel extends JPanel { private static final ImageIcon NOTIFY_ICON = new ImageIcon(ImageUtil.loadImageResource(TimeTrackingPlugin.class, "notify_icon.png")); private static final ImageIcon NOTIFY_SELECTED_ICON = new ImageIcon(ImageUtil.loadImageResource(TimeTrackingPlugin.class, "notify_selected_icon.png")); + private static final Rectangle OVERLAY_ICON_BOUNDS; + + static + { + int width = Constants.ITEM_SPRITE_WIDTH * 2 / 3; + int height = Constants.ITEM_SPRITE_HEIGHT * 2 / 3; + int x = Constants.ITEM_SPRITE_WIDTH - width; + int y = Constants.ITEM_SPRITE_HEIGHT - height; + OVERLAY_ICON_BOUNDS = new Rectangle(x, y, width, height); + } private final T timeable; private final JLabel icon = new JLabel(); + private final JLabel overlayIcon = new JLabel(); private final JLabel farmingContractIcon = new JLabel(); private final JToggleButton notifyButton = new JToggleButton(); private final JLabel estimate = new JLabel(); @@ -70,6 +84,7 @@ public class TimeablePanel extends JPanel topContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); icon.setMinimumSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT)); + overlayIcon.setMinimumSize(OVERLAY_ICON_BOUNDS.getSize()); farmingContractIcon.setMinimumSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT)); JPanel infoPanel = new JPanel(); @@ -105,8 +120,15 @@ public class TimeablePanel extends JPanel iconPanel.add(notifyPanel, BorderLayout.EAST); iconPanel.add(farmingContractIcon, BorderLayout.WEST); + JLayeredPane layeredIconPane = new JLayeredPane(); + layeredIconPane.setPreferredSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT)); + layeredIconPane.add(icon, Integer.valueOf(0)); + layeredIconPane.add(overlayIcon, Integer.valueOf(1)); + icon.setBounds(0, 0, Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT); + overlayIcon.setBounds(OVERLAY_ICON_BOUNDS); + topContainer.add(iconPanel, BorderLayout.EAST); - topContainer.add(icon, BorderLayout.WEST); + topContainer.add(layeredIconPane, BorderLayout.WEST); topContainer.add(infoPanel, BorderLayout.CENTER); progress.setValue(0); @@ -115,4 +137,21 @@ public class TimeablePanel extends JPanel add(topContainer, BorderLayout.NORTH); add(progress, BorderLayout.SOUTH); } + + public void setOverlayIconImage(BufferedImage overlayImg) + { + if (overlayImg == null) + { + overlayIcon.setIcon(null); + return; + } + + if (OVERLAY_ICON_BOUNDS.width != overlayImg.getWidth() || OVERLAY_ICON_BOUNDS.height != overlayImg.getHeight()) + { + overlayImg = ImageUtil.resizeImage(overlayImg, OVERLAY_ICON_BOUNDS.width, OVERLAY_ICON_BOUNDS.height); + } + + overlayIcon.setIcon(new ImageIcon(overlayImg)); + } + } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostState.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostState.java new file mode 100644 index 0000000000..659e29a282 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostState.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 LlemonDuck + * 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.timetracking.farming; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.ItemID; + +@RequiredArgsConstructor +@Getter +public enum CompostState +{ + + COMPOST(ItemID.COMPOST), + SUPERCOMPOST(ItemID.SUPERCOMPOST), + ULTRACOMPOST(ItemID.ULTRACOMPOST), + ; + + private final int itemId; + +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostTracker.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostTracker.java new file mode 100644 index 0000000000..b5066658f6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/CompostTracker.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2022 LlemonDuck + * 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.timetracking.farming; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.ItemID; +import net.runelite.api.ObjectComposition; +import net.runelite.api.Tile; +import net.runelite.api.annotations.Varbit; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.timetracking.TimeTrackingConfig; + +@Singleton +@Slf4j +@RequiredArgsConstructor(onConstructor = @__(@Inject)) +public class CompostTracker +{ + + @Value + @VisibleForTesting + static class PendingCompost + { + Instant timeout; + WorldPoint patchLocation; + FarmingPatch farmingPatch; + } + + private static final Duration COMPOST_ACTION_TIMEOUT = Duration.ofSeconds(30); + + private static final Pattern COMPOST_USED_ON_PATCH = Pattern.compile( + "You treat the .+ with (?ultra|super|)compost\\."); + private static final Pattern FERTILE_SOIL_CAST = Pattern.compile( + "The .+ has been treated with (?ultra|super|)compost\\."); + private static final Pattern ALREADY_TREATED = Pattern.compile( + "This .+ has already been (treated|fertilised) with (?ultra|super|)compost(?: - the spell can't make it any more fertile)?\\."); + private static final Pattern INSPECT_PATCH = Pattern.compile( + "This is an? .+\\. The soil has been treated with (?ultra|super|)compost\\..*"); + + private static final ImmutableSet COMPOST_ITEMS = ImmutableSet.of( + ItemID.COMPOST, + ItemID.SUPERCOMPOST, + ItemID.ULTRACOMPOST, + ItemID.BOTTOMLESS_COMPOST_BUCKET_22997 + ); + + private final Client client; + private final FarmingWorld farmingWorld; + private final ConfigManager configManager; + + @VisibleForTesting + final Map pendingCompostActions = new HashMap<>(); + + private static String configKey(FarmingPatch fp) + { + return fp.configKey() + "." + TimeTrackingConfig.COMPOST; + } + + public void setCompostState(FarmingPatch fp, CompostState state) + { + log.debug("Storing compost state [{}] for patch [{}]", state, fp); + if (state == null) + { + configManager.unsetRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, configKey(fp)); + } + else + { + configManager.setRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, configKey(fp), state); + } + } + + public CompostState getCompostState(FarmingPatch fp) + { + return configManager.getRSProfileConfiguration( + TimeTrackingConfig.CONFIG_GROUP, + configKey(fp), + CompostState.class + ); + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked e) + { + if (!isCompostAction(e)) + { + return; + } + + ObjectComposition patchDef = client.getObjectDefinition(e.getId()); + WorldPoint actionLocation = WorldPoint.fromScene(client, e.getParam0(), e.getParam1(), client.getPlane()); + FarmingPatch targetPatch = farmingWorld.getRegionsForLocation(actionLocation) + .stream() + .flatMap(fr -> Arrays.stream(fr.getPatches())) + .filter(fp -> fp.getVarbit() == patchDef.getVarbitId()) + .findFirst() + .orElse(null); + if (targetPatch == null) + { + return; + } + + log.debug("Storing pending compost action for patch [{}]", targetPatch); + PendingCompost pc = new PendingCompost( + Instant.now().plus(COMPOST_ACTION_TIMEOUT), + actionLocation, + targetPatch + ); + pendingCompostActions.put(targetPatch, pc); + } + + private boolean isCompostAction(MenuOptionClicked e) + { + switch (e.getMenuAction()) + { + case WIDGET_TARGET_ON_GAME_OBJECT: + Widget w = client.getSelectedWidget(); + assert w != null; + return COMPOST_ITEMS.contains(w.getItemId()) || w.getId() == WidgetInfo.SPELL_LUNAR_FERTILE_SOIL.getPackedId(); + + case GAME_OBJECT_FIRST_OPTION: + case GAME_OBJECT_SECOND_OPTION: + case GAME_OBJECT_THIRD_OPTION: + case GAME_OBJECT_FOURTH_OPTION: + case GAME_OBJECT_FIFTH_OPTION: + return "Inspect".equals(e.getMenuOption()); + + default: + return false; + } + } + + @Subscribe + public void onChatMessage(ChatMessage e) + { + if (e.getType() != ChatMessageType.GAMEMESSAGE && e.getType() != ChatMessageType.SPAM) + { + return; + } + + CompostState compostUsed = determineCompostUsed(e.getMessage()); + if (compostUsed == null) + { + return; + } + + this.expirePendingActions(); + + pendingCompostActions.values() + .stream() + .filter(this::playerIsBesidePatch) + .findFirst() + .ifPresent(pc -> + { + setCompostState(pc.getFarmingPatch(), compostUsed); + pendingCompostActions.remove(pc.getFarmingPatch()); + }); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged e) + { + switch (e.getGameState()) + { + case LOGGED_IN: + case LOADING: + return; + + default: + pendingCompostActions.clear(); + } + } + + private boolean playerIsBesidePatch(PendingCompost pendingCompost) + { + // find gameobject instance in scene + // it is possible that the scene has reloaded between use and action occurring so we use worldpoint + // instead of storing scene coords in the menuoptionclicked event + LocalPoint localPatchLocation = LocalPoint.fromWorld(client, pendingCompost.getPatchLocation()); + if (localPatchLocation == null) + { + return false; + } + + @Varbit int patchVarb = pendingCompost.getFarmingPatch().getVarbit(); + Tile patchTile = client.getScene() + .getTiles()[client.getPlane()][localPatchLocation.getSceneX()][localPatchLocation.getSceneY()]; + GameObject patchObject = null; + for (GameObject go : patchTile.getGameObjects()) + { + if (go != null && client.getObjectDefinition(go.getId()).getVarbitId() == patchVarb) + { + patchObject = go; + break; + } + } + assert patchObject != null; + + // player coords + final WorldPoint playerPos = client.getLocalPlayer().getWorldLocation(); + final int playerX = playerPos.getX(); + final int playerY = playerPos.getY(); + + // patch coords + final WorldPoint patchBase = pendingCompost.getPatchLocation(); + final int minX = patchBase.getX(); + final int minY = patchBase.getY(); + final int maxX = minX + patchObject.sizeX() - 1; + final int maxY = minY + patchObject.sizeY() - 1; + + // player should be within one tile of these coords + return playerX >= (minX - 1) && playerX <= (maxX + 1) && playerY >= (minY - 1) && playerY <= (maxY + 1); + } + + private void expirePendingActions() + { + pendingCompostActions.values().removeIf(e -> Instant.now().isAfter(e.getTimeout())); + } + + @VisibleForTesting + static CompostState determineCompostUsed(String chatMessage) + { + if (!chatMessage.contains("compost")) + { + return null; + } + + Matcher matcher; + if ((matcher = COMPOST_USED_ON_PATCH.matcher(chatMessage)).matches() || + (matcher = FERTILE_SOIL_CAST.matcher(chatMessage)).matches() || + (matcher = ALREADY_TREATED.matcher(chatMessage)).matches() || + (matcher = INSPECT_PATCH.matcher(chatMessage)).matches()) + { + String compostGroup = matcher.group("compostType"); + switch (compostGroup) + { + case "ultra": + return CompostState.ULTRACOMPOST; + case "super": + return CompostState.SUPERCOMPOST; + default: + return CompostState.COMPOST; + } + } + + return null; + } + +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java index 835f17e463..1599393ec8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingPatch.java @@ -28,6 +28,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import lombok.ToString; import net.runelite.api.annotations.Varbit; import net.runelite.client.plugins.timetracking.TimeTrackingConfig; @@ -35,13 +36,17 @@ import net.runelite.client.plugins.timetracking.TimeTrackingConfig; access = AccessLevel.PACKAGE ) @Getter +@ToString(onlyExplicitlyIncluded = true) class FarmingPatch { @Setter(AccessLevel.PACKAGE) + @ToString.Include private FarmingRegion region; + @ToString.Include private final String name; @Getter(onMethod_ = {@Varbit}) private final int varbit; + @ToString.Include private final PatchImplementation implementation; String configKey() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java index 1d16cbeae5..cc30311eef 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTabPanel.java @@ -43,10 +43,12 @@ import net.runelite.client.plugins.timetracking.TimeTrackingConfig; import net.runelite.client.plugins.timetracking.TimeablePanel; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; +import net.runelite.client.util.AsyncBufferedImage; public class FarmingTabPanel extends TabContentPanel { private final FarmingTracker farmingTracker; + private final CompostTracker compostTracker; private final ItemManager itemManager; private final ConfigManager configManager; private final TimeTrackingConfig config; @@ -55,6 +57,7 @@ public class FarmingTabPanel extends TabContentPanel FarmingTabPanel( FarmingTracker farmingTracker, + CompostTracker compostTracker, ItemManager itemManager, ConfigManager configManager, TimeTrackingConfig config, @@ -63,6 +66,7 @@ public class FarmingTabPanel extends TabContentPanel ) { this.farmingTracker = farmingTracker; + this.compostTracker = compostTracker; this.itemManager = itemManager; this.configManager = configManager; this.config = config; @@ -131,7 +135,6 @@ public class FarmingTabPanel extends TabContentPanel p.setBorder(null); } } - } @Override @@ -150,10 +153,26 @@ public class FarmingTabPanel extends TabContentPanel FarmingPatch patch = panel.getTimeable(); PatchPrediction prediction = farmingTracker.predictPatch(patch); + CompostState compostState = compostTracker.getCompostState(patch); + String compostTooltip = ""; + if (compostState != null) + { + AsyncBufferedImage compostImg = itemManager.getImage(compostState.getItemId()); + Runnable compostOverlayRunnable = () -> panel.setOverlayIconImage(compostImg); + compostImg.onLoaded(compostOverlayRunnable); + compostOverlayRunnable.run(); + + compostTooltip = " with " + compostState.name().toLowerCase(); + } + else + { + panel.setOverlayIconImage(null); + } + if (prediction == null) { itemManager.getImage(Produce.WEEDS.getItemID()).addTo(panel.getIcon()); - panel.getIcon().setToolTipText("Unknown state"); + panel.getIcon().setToolTipText("Unknown state" + compostTooltip); panel.getProgress().setMaximumValue(0); panel.getProgress().setValue(0); panel.getProgress().setVisible(false); @@ -165,12 +184,12 @@ public class FarmingTabPanel extends TabContentPanel if (prediction.getProduce().getItemID() < 0) { panel.getIcon().setIcon(null); - panel.getIcon().setToolTipText("Unknown state"); + panel.getIcon().setToolTipText("Unknown state" + compostTooltip); } else { itemManager.getImage(prediction.getProduce().getItemID()).addTo(panel.getIcon()); - panel.getIcon().setToolTipText(prediction.getProduce().getName()); + panel.getIcon().setToolTipText(prediction.getProduce().getName() + compostTooltip); } switch (prediction.getCropState()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java index fe314c46fa..ad4df62e37 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/FarmingTracker.java @@ -63,6 +63,7 @@ public class FarmingTracker private final TimeTrackingConfig config; private final FarmingWorld farmingWorld; private final Notifier notifier; + private final CompostTracker compostTracker; private final Map summaries = new EnumMap<>(Tab.class); @@ -78,7 +79,7 @@ public class FarmingTracker private boolean firstNotifyCheck = true; @Inject - private FarmingTracker(Client client, ItemManager itemManager, ConfigManager configManager, TimeTrackingConfig config, FarmingWorld farmingWorld, Notifier notifier) + private FarmingTracker(Client client, ItemManager itemManager, ConfigManager configManager, TimeTrackingConfig config, FarmingWorld farmingWorld, Notifier notifier, CompostTracker compostTracker) { this.client = client; this.itemManager = itemManager; @@ -86,11 +87,12 @@ public class FarmingTracker this.config = config; this.farmingWorld = farmingWorld; this.notifier = notifier; + this.compostTracker = compostTracker; } public FarmingTabPanel createTabPanel(Tab tab, FarmingContractManager farmingContractManager) { - return new FarmingTabPanel(this, itemManager, configManager, config, farmingWorld.getTabs().get(tab), farmingContractManager); + return new FarmingTabPanel(this, compostTracker, itemManager, configManager, config, farmingWorld.getTabs().get(tab), farmingContractManager); } /** @@ -148,6 +150,12 @@ public class FarmingTracker String strVarbit = Integer.toString(client.getVarbitValue(varbit)); String storedValue = configManager.getRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, key); + PatchState currentPatchState = patch.getImplementation().forVarbitValue(client.getVarbitValue(varbit)); + if (currentPatchState == null) + { + continue; + } + if (storedValue != null) { String[] parts = storedValue.split(":"); @@ -172,9 +180,8 @@ public class FarmingTracker else if (!newRegionLoaded && timeSinceModalClose > 1) { PatchState previousPatchState = patch.getImplementation().forVarbitValue(Integer.parseInt(parts[0])); - PatchState currentPatchState = patch.getImplementation().forVarbitValue(client.getVarbitValue(varbit)); - if (previousPatchState == null || currentPatchState == null) + if (previousPatchState == null) { continue; } @@ -217,6 +224,11 @@ public class FarmingTracker } } + if (currentPatchState.getCropState() == CropState.DEAD || currentPatchState.getCropState() == CropState.HARVESTABLE) + { + compostTracker.setCompostState(patch, null); + } + String value = strVarbit + ":" + unixNow; configManager.setRSProfileConfiguration(TimeTrackingConfig.CONFIG_GROUP, key, value); changed = true; diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/CompostTrackerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/CompostTrackerTest.java new file mode 100644 index 0000000000..42dba61b5d --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/timetracking/farming/CompostTrackerTest.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2022 LlemonDuck + * 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.timetracking.farming; + +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.time.Instant; +import java.util.Collections; +import javax.inject.Inject; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.ItemID; +import net.runelite.api.MenuAction; +import net.runelite.api.ObjectComposition; +import net.runelite.api.Player; +import net.runelite.api.Scene; +import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigManager; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; +import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.Mock; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CompostTrackerTest +{ + + @Inject + private CompostTracker compostTracker; + + @Mock + @Bind + private Client client; + + @Mock + @Bind + private FarmingWorld farmingWorld; + + @Mock + @Bind + private ConfigManager configManager; + + @Mock + @Bind + private FarmingRegion farmingRegion; + + @Mock + @Bind + private FarmingPatch farmingPatch; + + @Mock + @Bind + private GameObject patchObject; + + @Mock + @Bind + private Player player; + + @Mock + @Bind + private Scene scene; + + @Mock + @Bind + private Tile tile; + + @Mock + @Bind + private ObjectComposition patchDef; + + @Rule + public ErrorCollector collector = new ErrorCollector(); + + private static final int PATCH_ID = 12345; + private static final int PATCH_VARBIT = 54321; + private static final WorldPoint worldPoint = new WorldPoint(1, 2, 0); + + @Before + public void before() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + compostTracker.pendingCompostActions.clear(); + + when(client.getBaseX()).thenReturn(0); + when(client.getBaseY()).thenReturn(0); + when(client.getPlane()).thenReturn(0); + when(client.getLocalPlayer()).thenReturn(player); + when(player.getWorldLocation()).thenReturn(worldPoint); + when(client.getScene()).thenReturn(scene); + when(client.getObjectDefinition(PATCH_ID)).thenReturn(patchDef); + + when(scene.getTiles()).thenReturn(new Tile[][][]{{null, {null, null, tile}}}); // indices match worldPoint + when(tile.getGameObjects()).thenReturn(new GameObject[]{patchObject}); + + when(farmingWorld.getRegionsForLocation(any())).thenReturn(Collections.singleton(farmingRegion)); + + when(farmingRegion.getPatches()).thenReturn(new FarmingPatch[]{farmingPatch}); + + when(farmingPatch.getVarbit()).thenReturn(PATCH_VARBIT); + when(farmingPatch.configKey()).thenReturn("MOCK"); + + when(patchObject.getId()).thenReturn(PATCH_ID); + when(patchObject.sizeX()).thenReturn(1); + when(patchObject.sizeY()).thenReturn(1); + + when(patchDef.getVarbitId()).thenReturn(PATCH_VARBIT); + } + + @Test + public void setCompostState_storesNonNullChangesToConfig() + { + compostTracker.setCompostState(farmingPatch, CompostState.COMPOST); + verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.COMPOST); + } + + @Test + public void setCompostState_storesNullChangesByClearingConfig() + { + compostTracker.setCompostState(farmingPatch, null); + verify(configManager).unsetRSProfileConfiguration("timetracking", "MOCK.compost"); + } + + @Test + public void getCompostState_directlyReturnsFromConfig() + { + when(configManager.getRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.class)).thenReturn( + CompostState.SUPERCOMPOST); + assertThat(compostTracker.getCompostState(farmingPatch), is(CompostState.SUPERCOMPOST)); + } + + @Test + public void determineCompostUsed_returnsAppropriateCompostValues() + { + // invalid + collector.checkThat( + CompostTracker.determineCompostUsed("This is not a farming chat message."), + is((CompostState) null) + ); + collector.checkThat( + CompostTracker.determineCompostUsed("Contains word compost but is not examine message."), + is((CompostState) null) + ); + + // inspect + collector.checkThat( + CompostTracker.determineCompostUsed("This is an allotment. The soil has been treated with supercompost. The patch is empty and weeded."), + is(CompostState.SUPERCOMPOST) + ); + + // fertile soil on existing patch + collector.checkThat( + CompostTracker.determineCompostUsed("This patch has already been fertilised with ultracompost - the spell can't make it any more fertile."), + is(CompostState.ULTRACOMPOST) + ); + // fertile soil on cleared patch + collector.checkThat( + CompostTracker.determineCompostUsed("The herb patch has been treated with supercompost."), + is(CompostState.SUPERCOMPOST) + ); + + // bucket on cleared patch + collector.checkThat( + CompostTracker.determineCompostUsed("You treat the herb patch with ultracompost."), + is(CompostState.ULTRACOMPOST) + ); + collector.checkThat( + CompostTracker.determineCompostUsed("You treat the tree patch with compost."), + is(CompostState.COMPOST) + ); + collector.checkThat( + CompostTracker.determineCompostUsed("You treat the fruit tree patch with supercompost."), + is(CompostState.SUPERCOMPOST) + ); + } + + @Test + public void onMenuOptionClicked_queuesPendingCompostForInspectActions() + { + MenuOptionClicked inspectPatchAction = mock(MenuOptionClicked.class); + when(inspectPatchAction.getMenuAction()).thenReturn(MenuAction.GAME_OBJECT_SECOND_OPTION); + when(inspectPatchAction.getMenuOption()).thenReturn("Inspect"); + when(inspectPatchAction.getId()).thenReturn(PATCH_ID); + when(inspectPatchAction.getParam0()).thenReturn(1); + when(inspectPatchAction.getParam1()).thenReturn(2); + + compostTracker.onMenuOptionClicked(inspectPatchAction); + CompostTracker.PendingCompost actual = compostTracker.pendingCompostActions.get(farmingPatch); + + assertThat(actual.getFarmingPatch(), is(farmingPatch)); + assertThat(actual.getPatchLocation(), is(new WorldPoint(1, 2, 0))); + } + + @Test + public void onMenuOptionClicked_queuesPendingCompostForCompostActions() + { + Widget widget = mock(Widget.class); + when(client.getSelectedWidget()).thenReturn(widget); + when(widget.getItemId()).thenReturn(ItemID.ULTRACOMPOST); + + MenuOptionClicked inspectPatchAction = mock(MenuOptionClicked.class); + when(inspectPatchAction.getMenuAction()).thenReturn(MenuAction.WIDGET_TARGET_ON_GAME_OBJECT); + when(inspectPatchAction.getId()).thenReturn(PATCH_ID); + when(inspectPatchAction.getParam0()).thenReturn(1); + when(inspectPatchAction.getParam1()).thenReturn(2); + + compostTracker.onMenuOptionClicked(inspectPatchAction); + CompostTracker.PendingCompost actual = compostTracker.pendingCompostActions.get(farmingPatch); + + assertThat(actual.getFarmingPatch(), is(farmingPatch)); + assertThat(actual.getPatchLocation(), is(new WorldPoint(1, 2, 0))); + } + + @Test + public void onMenuOptionClicked_queuesPendingCompostForFertileSoilSpellActions() + { + Widget widget = mock(Widget.class); + when(client.getSelectedWidget()).thenReturn(widget); + when(widget.getId()).thenReturn(WidgetInfo.SPELL_LUNAR_FERTILE_SOIL.getPackedId()); + + MenuOptionClicked inspectPatchAction = mock(MenuOptionClicked.class); + when(inspectPatchAction.getMenuAction()).thenReturn(MenuAction.WIDGET_TARGET_ON_GAME_OBJECT); + when(inspectPatchAction.getId()).thenReturn(PATCH_ID); + when(inspectPatchAction.getParam0()).thenReturn(1); + when(inspectPatchAction.getParam1()).thenReturn(2); + + compostTracker.onMenuOptionClicked(inspectPatchAction); + CompostTracker.PendingCompost actual = compostTracker.pendingCompostActions.get(farmingPatch); + + assertThat(actual.getFarmingPatch(), is(farmingPatch)); + assertThat(actual.getPatchLocation(), is(new WorldPoint(1, 2, 0))); + } + + @Test + public void onChatMessage_ignoresInvalidTypes() + { + ChatMessage chatEvent = mock(ChatMessage.class); + when(chatEvent.getType()).thenReturn(ChatMessageType.PUBLICCHAT); + + compostTracker.onChatMessage(chatEvent); + + verifyNoInteractions(client); + verifyNoInteractions(farmingWorld); + } + + @Test + public void onChatMessage_handlesInspectMessages() + { + ChatMessage chatEvent = mock(ChatMessage.class); + when(chatEvent.getType()).thenReturn(ChatMessageType.SPAM); + when(chatEvent.getMessage()).thenReturn("This is a tree patch. The soil has been treated with ultracompost. The patch is empty and weeded."); + + compostTracker.pendingCompostActions.put(farmingPatch, new CompostTracker.PendingCompost(Instant.MAX, worldPoint, farmingPatch)); + compostTracker.onChatMessage(chatEvent); + + verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.ULTRACOMPOST); + } + + @Test + public void onChatMessage_handlesBucketUseMessages() + { + ChatMessage chatEvent = mock(ChatMessage.class); + when(chatEvent.getType()).thenReturn(ChatMessageType.SPAM); + when(chatEvent.getMessage()).thenReturn("You treat the herb patch with compost."); + + compostTracker.pendingCompostActions.put(farmingPatch, new CompostTracker.PendingCompost(Instant.MAX, worldPoint, farmingPatch)); + compostTracker.onChatMessage(chatEvent); + + verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.COMPOST); + } + + @Test + public void onChatMessage_handlesFertileSoilMessages() + { + ChatMessage chatEvent = mock(ChatMessage.class); + when(chatEvent.getType()).thenReturn(ChatMessageType.SPAM); + when(chatEvent.getMessage()).thenReturn("The allotment has been treated with supercompost."); + + compostTracker.pendingCompostActions.put(farmingPatch, new CompostTracker.PendingCompost(Instant.MAX, worldPoint, farmingPatch)); + compostTracker.onChatMessage(chatEvent); + + verify(configManager).setRSProfileConfiguration("timetracking", "MOCK.compost", CompostState.SUPERCOMPOST); + } + +} From 28f45cedb5789f1483ed319d1974d0ad6d38de19 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sat, 7 May 2022 19:56:59 -0600 Subject: [PATCH 22/29] Perspective: fix clickbox for type 11 objects --- .../main/java/net/runelite/api/Perspective.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/runelite-api/src/main/java/net/runelite/api/Perspective.java b/runelite-api/src/main/java/net/runelite/api/Perspective.java index 38a86eea7b..64ab37be46 100644 --- a/runelite-api/src/main/java/net/runelite/api/Perspective.java +++ b/runelite-api/src/main/java/net/runelite/api/Perspective.java @@ -719,19 +719,13 @@ public class Perspective private static SimplePolygon calculateAABB(Client client, Model m, int jauOrient, int x, int y, int z) { - int ex = m.getExtremeX(); - if (ex == -1) - { - // dynamic models don't get stored when they render where this normally happens - m.calculateBoundsCylinder(); - m.calculateExtreme(0); - ex = m.getExtremeX(); - } + m.calculateExtreme(jauOrient); int x1 = m.getCenterX(); int y1 = m.getCenterZ(); int z1 = m.getCenterY(); + int ex = m.getExtremeX(); int ey = m.getExtremeZ(); int ez = m.getExtremeY(); @@ -759,7 +753,7 @@ public class Perspective int[] x2d = new int[8]; int[] y2d = new int[8]; - modelToCanvas(client, 8, x, y, z, jauOrient, xa, ya, za, x2d, y2d); + modelToCanvasCpu(client, 8, x, y, z, 0, xa, ya, za, x2d, y2d); return Jarvis.convexHull(x2d, y2d); } @@ -770,7 +764,7 @@ public class Perspective int[] y2d = new int[m.getVerticesCount()]; final int[] faceColors3 = m.getFaceColors3(); - Perspective.modelToCanvas(client, + Perspective.modelToCanvasCpu(client, m.getVerticesCount(), x, y, z, jauOrient, From cab126c6bb7d5a33d1bd84239bfc0aa3e2e39d34 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 9 May 2022 21:10:16 -0400 Subject: [PATCH 23/29] timers: remove unused tzhaar complete matcher The usage of matches() vs find() is incorrect, but the timer is already removed when teleported from the arena --- .../java/net/runelite/client/plugins/timers/TimersPlugin.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java index 4b4be20c74..07e7029b99 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java @@ -137,7 +137,6 @@ public class TimersPlugin extends Plugin private static final int NMZ_MAP_REGION_ID = 9033; private static final Pattern TZHAAR_WAVE_MESSAGE = Pattern.compile("Wave: (\\d+)"); private static final String TZHAAR_DEFEATED_MESSAGE = "You have been defeated!"; - private static final Pattern TZHAAR_COMPLETE_MESSAGE = Pattern.compile("Your (?:TzTok-Jad|TzKal-Zuk) kill count is:"); private static final Pattern TZHAAR_PAUSED_MESSAGE = Pattern.compile("The (?:Inferno|Fight Cave) has been paused. You may now log out."); private TimerTimer freezeTimer; @@ -776,7 +775,7 @@ public class TimersPlugin extends Plugin } } - if (message.equals(TZHAAR_DEFEATED_MESSAGE) || TZHAAR_COMPLETE_MESSAGE.matcher(message).matches()) + if (message.equals(TZHAAR_DEFEATED_MESSAGE)) { log.debug("Stopping tzhaar timer"); removeTzhaarTimer(); From b5b19e0771e62e985f481356c590e930e4f45eee Mon Sep 17 00:00:00 2001 From: Max Weber Date: Tue, 10 May 2022 06:43:05 -0600 Subject: [PATCH 24/29] rl-api: expose currently playing music/jingle --- .../src/main/java/net/runelite/api/Client.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 b38a78d338..7972c54edc 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -1167,6 +1167,17 @@ public interface Client extends OAuthApi, GameEngine */ void setMusicVolume(int volume); + /** + * @return true if the current {@link #getMusicCurrentTrackId()} is a Jingle, otherwise its a Track + */ + boolean isPlayingJingle(); + + /** + * @return Currently playing music/jingle id, or -1 if not playing + * @see #isPlayingJingle() + */ + int getMusicCurrentTrackId(); + /** * Play a sound effect at the player's current location. This is how UI, * and player-generated (e.g. mining, woodcutting) sound effects are From 82c2d61a1462bd936cef1f4f538bed429dfe2690 Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 10 May 2022 14:59:57 -0400 Subject: [PATCH 25/29] clues: Fix capitalization for Dark Mage anagram --- .../client/plugins/cluescrolls/clues/AnagramClue.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/AnagramClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/AnagramClue.java index 4e33b88bc5..cb88c900d3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/AnagramClue.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/AnagramClue.java @@ -230,8 +230,8 @@ public class AnagramClue extends ClueScroll implements TextClueScroll, NpcClueSc .build(), AnagramClue.builder() .text("DEKAGRAM") - .npc("Dark mage") - .location(new WorldPoint(3039, 4835, 0)) + .npc("Dark Mage") + .location(new WorldPoint(3039, 4834, 0)) .area("Centre of the Abyss") .question("How many rifts are found here in the abyss?") .answer("13") From ada6d99a492b06a7d1bcfcbc8cbc9aaed0a22573 Mon Sep 17 00:00:00 2001 From: SirGirion Date: Tue, 10 May 2022 21:05:19 -0400 Subject: [PATCH 26/29] timetracking: fix tick rate of teak trees --- .../runelite/client/plugins/timetracking/farming/Produce.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java index bfb5a1abe3..203189e3ec 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/farming/Produce.java @@ -113,7 +113,7 @@ public enum Produce POTATO_CACTUS("Potato cactus", "Potato cacti", PatchImplementation.CACTUS, ItemID.POTATO_CACTUS, 10, 8, 5, 7), // Hardwood - TEAK("Teak", PatchImplementation.HARDWOOD_TREE, ItemID.TEAK_LOGS, 560, 8), + TEAK("Teak", PatchImplementation.HARDWOOD_TREE, ItemID.TEAK_LOGS, 640, 8), MAHOGANY("Mahogany", PatchImplementation.HARDWOOD_TREE, ItemID.MAHOGANY_LOGS, 640, 9), // Anima From 05029eb859eaa354116311a67b89e7310544eb01 Mon Sep 17 00:00:00 2001 From: Stephen Au <32289727+stephenisau@users.noreply.github.com> Date: Tue, 10 May 2022 18:07:02 -0700 Subject: [PATCH 27/29] spriteid: fix Guthix typo --- runelite-api/src/main/java/net/runelite/api/SpriteID.java | 4 ++-- .../client/plugins/skillcalculator/skills/MagicAction.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 cb7cd1b560..b8e0e20662 100644 --- a/runelite-api/src/main/java/net/runelite/api/SpriteID.java +++ b/runelite-api/src/main/java/net/runelite/api/SpriteID.java @@ -89,7 +89,7 @@ public final class SpriteID public static final int SPELL_ENFEEBLE = 57; public static final int SPELL_STUN = 58; public static final int SPELL_FLAMES_OF_ZAMORAK = 59; - public static final int SPELL_CLAWS_OF_GUTHIC = 60; + public static final int SPELL_CLAWS_OF_GUTHIX = 60; public static final int SPELL_SARADOMIN_STRIKE = 61; public static final int UNUSED_SPELL_CALL_ANIMAL = 62; public static final int UNUSED_SPELL_RAISE_SKELETON = 63; @@ -139,7 +139,7 @@ public final class SpriteID public static final int SPELL_ENFEEBLE_DISABLED = 107; public static final int SPELL_STUN_DISABLED = 108; public static final int SPELL_FLAMES_OF_ZAMORAK_DISABLED = 109; - public static final int SPELL_CLAWS_OF_GUTHIC_DISABLED = 110; + public static final int SPELL_CLAWS_OF_GUTHIX_DISABLED = 110; public static final int SPELL_SARADOMIN_STRIKE_DISABLED = 111; public static final int UNUSED_SPELL_CALL_ANIMAL_DISABLED = 112; public static final int UNUSED_SPELL_RAISE_SKELETON_DISABLED = 113; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/MagicAction.java b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/MagicAction.java index e907ebb48a..3f778534c4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/MagicAction.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/MagicAction.java @@ -100,7 +100,7 @@ public enum MagicAction implements SkillAction FIRE_BLAST("Fire Blast", 59, 34.5f, SpriteID.SPELL_FIRE_BLAST), MARK_OF_DARKNESS("Mark of Darkness", 59, 70, SpriteID.SPELL_MARK_OF_DARKNESS), SENNTISTEN_TELEPORT("Senntisten Teleport", 60, 70, SpriteID.SPELL_SENNTISTEN_TELEPORT), - CLAWS_OF_GUTHIX("Claws Of Guthix", 60, 35, SpriteID.SPELL_CLAWS_OF_GUTHIC), + CLAWS_OF_GUTHIX("Claws Of Guthix", 60, 35, SpriteID.SPELL_CLAWS_OF_GUTHIX), FLAMES_OF_ZAMORAK("Flames Of Zamorak", 60, 35, SpriteID.SPELL_FLAMES_OF_ZAMORAK), SARADOMIN_STRIKE("Saradomin Strike", 60, 35, SpriteID.SPELL_SARADOMIN_STRIKE), CHARGE_EARTH_ORB("Charge Earth Orb", 60, 70, SpriteID.SPELL_CHARGE_EARTH_ORB), From 59b60655f757aaeb15c8aa43a66e7ca274275d62 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 10 May 2022 22:29:07 -0400 Subject: [PATCH 28/29] status bars: fix npe if sprites aren't loaded yet --- .../plugins/statusbars/StatusBarsOverlay.java | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/statusbars/StatusBarsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/statusbars/StatusBarsOverlay.java index 3d5cd8a264..e414b794d4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/statusbars/StatusBarsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/statusbars/StatusBarsOverlay.java @@ -29,9 +29,9 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; +import java.awt.image.BufferedImage; import java.util.EnumMap; import java.util.Map; -import java.util.Objects; import javax.inject.Inject; import net.runelite.api.Client; import net.runelite.api.MenuEntry; @@ -39,8 +39,8 @@ import net.runelite.api.Point; import net.runelite.api.Prayer; import net.runelite.api.Skill; import net.runelite.api.SpriteID; -import net.runelite.api.Varbits; import net.runelite.api.VarPlayer; +import net.runelite.api.Varbits; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.game.AlternateSprites; @@ -86,10 +86,10 @@ class StatusBarsOverlay extends Overlay private final SpriteManager spriteManager; private final Image prayerIcon; + private final Image heartDisease; + private final Image heartPoison; + private final Image heartVenom; private Image heartIcon; - private Image heartDisease; - private Image heartPoison; - private Image heartVenom; private Image specialIcon; private Image energyIcon; private final Map barRenderers = new EnumMap<>(BarMode.class); @@ -106,6 +106,10 @@ class StatusBarsOverlay extends Overlay this.spriteManager = spriteManager; prayerIcon = ImageUtil.resizeCanvas(ImageUtil.resizeImage(skillIconManager.getSkillImage(Skill.PRAYER, true), IMAGE_SIZE, IMAGE_SIZE), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); + heartDisease = ImageUtil.resizeCanvas(ImageUtil.loadImageResource(AlternateSprites.class, AlternateSprites.DISEASE_HEART), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); + heartPoison = ImageUtil.resizeCanvas(ImageUtil.loadImageResource(AlternateSprites.class, AlternateSprites.POISON_HEART), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); + heartVenom = ImageUtil.resizeCanvas(ImageUtil.loadImageResource(AlternateSprites.class, AlternateSprites.VENOM_HEART), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); + initRenderers(); } @@ -321,16 +325,28 @@ class StatusBarsOverlay extends Overlay private void buildIcons() { - if (heartIcon != null && heartDisease != null && heartPoison != null && heartVenom != null && energyIcon != null && specialIcon != null) + if (heartIcon == null) { - return; + heartIcon = loadAndResize(SpriteID.MINIMAP_ORB_HITPOINTS_ICON); + } + if (energyIcon == null) + { + energyIcon = loadAndResize(SpriteID.MINIMAP_ORB_WALK_ICON); + } + if (specialIcon == null) + { + specialIcon = loadAndResize(SpriteID.MINIMAP_ORB_SPECIAL_ICON); + } + } + + private BufferedImage loadAndResize(int spriteId) + { + BufferedImage image = spriteManager.getSprite(spriteId, 0); + if (image == null) + { + return null; } - heartIcon = ImageUtil.resizeCanvas(Objects.requireNonNull(spriteManager.getSprite(SpriteID.MINIMAP_ORB_HITPOINTS_ICON, 0)), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); - heartDisease = ImageUtil.resizeCanvas(ImageUtil.loadImageResource(AlternateSprites.class, AlternateSprites.DISEASE_HEART), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); - heartPoison = ImageUtil.resizeCanvas(ImageUtil.loadImageResource(AlternateSprites.class, AlternateSprites.POISON_HEART), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); - heartVenom = ImageUtil.resizeCanvas(ImageUtil.loadImageResource(AlternateSprites.class, AlternateSprites.VENOM_HEART), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); - energyIcon = ImageUtil.resizeCanvas(Objects.requireNonNull(spriteManager.getSprite(SpriteID.MINIMAP_ORB_WALK_ICON, 0)), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); - specialIcon = ImageUtil.resizeCanvas(Objects.requireNonNull(spriteManager.getSprite(SpriteID.MINIMAP_ORB_SPECIAL_ICON, 0)), ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); + return ImageUtil.resizeCanvas(image, ICON_DIMENSIONS.width, ICON_DIMENSIONS.height); } } From 50af007e944570de1138ce99f52e16bd2d8faee2 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 10 May 2022 23:49:26 -0400 Subject: [PATCH 29/29] chat commands: fix matching random event rewards as kill messages --- .../plugins/chatcommands/ChatCommandsPlugin.java | 15 ++++++++++++--- .../chatcommands/ChatCommandsPluginTest.java | 12 ++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java index 67359d10ea..e0017e1d3d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java @@ -27,6 +27,7 @@ package net.runelite.client.plugins.chatcommands; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; @@ -107,7 +108,7 @@ import org.apache.commons.text.WordUtils; @Slf4j public class ChatCommandsPlugin extends Plugin { - private static final Pattern KILLCOUNT_PATTERN = Pattern.compile("Your (?:completion count for |subdued |completed )?(.+?) (?:(?:kill|harvest|lap|completion) )?(?:count )?is: (\\d+)"); + private static final Pattern KILLCOUNT_PATTERN = Pattern.compile("Your (?
completion count for |subdued |completed )?(?.+?) (?(?:(?:kill|harvest|lap|completion) )?(?:count )?)is: (?\\d+)");
 	private static final String TEAM_SIZES = "(?\\d+(?:\\+|-\\d+)? players?|Solo)";
 	private static final Pattern RAIDS_PB_PATTERN = Pattern.compile("Congratulations - your raid is complete!
Team size: " + TEAM_SIZES + " Duration: (?[0-9:]+(?:\\.[0-9]+)?) \\(new personal best\\)"); private static final Pattern RAIDS_DURATION_PATTERN = Pattern.compile("Congratulations - your raid is complete!
Team size: " + TEAM_SIZES + " Duration: [0-9:.]+ Personal best: (?[0-9:]+(?:\\.[0-9]+)?)"); @@ -388,8 +389,16 @@ public class ChatCommandsPlugin extends Plugin Matcher matcher = KILLCOUNT_PATTERN.matcher(message); if (matcher.find()) { - String boss = matcher.group(1); - int kc = Integer.parseInt(matcher.group(2)); + final String boss = matcher.group("boss"); + final int kc = Integer.parseInt(matcher.group("kc")); + final String pre = matcher.group("pre"); + final String post = matcher.group("post"); + + if (Strings.isNullOrEmpty(pre) && Strings.isNullOrEmpty(post)) + { + unsetKc(boss); + return; + } String renamedBoss = KILLCOUNT_RENAMES .getOrDefault(boss, boss) diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java index 14b92f8b1a..08090286cd 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java @@ -66,12 +66,15 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import org.mockito.Mock; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.mockito.junit.MockitoJUnitRunner; @@ -1156,4 +1159,13 @@ public class ChatCommandsPluginTest verify(configManager).setRSProfileConfiguration("killcount", "guardians of the rift", 167); } + + @Test + public void testReward() + { + ChatMessage chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "Your reward is: 1 x Kebab.", null, 0); + chatCommandsPlugin.onChatMessage(chatMessage); + + verify(configManager, never()).setRSProfileConfiguration(anyString(), anyString(), anyInt()); + } }