Merge pull request #3192 from open-osrs/upstream-1105

This commit is contained in:
Owain van Brakel
2022-05-11 12:05:23 +02:00
committed by GitHub
61 changed files with 2009 additions and 612 deletions

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -26,13 +26,20 @@ package net.runelite.cache;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
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;
import lombok.extern.slf4j.Slf4j;
import net.runelite.cache.definitions.AreaDefinition;
import net.runelite.cache.definitions.ObjectDefinition;
import net.runelite.cache.definitions.OverlayDefinition;
@@ -47,39 +54,44 @@ 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.Position;
import net.runelite.cache.region.Region;
import net.runelite.cache.region.RegionLoader;
import net.runelite.cache.util.Djb2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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)
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;
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}};
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;
private final Map<Integer, UnderlayDefinition> underlays = new HashMap<>();
private final Map<Integer, OverlayDefinition> overlays = new HashMap<>();
private final Map<Integer, Image> scaledMapIcons = new HashMap<>();
private SpriteDefinition[] mapDecorations;
private RegionLoader regionLoader;
private final RegionLoader regionLoader;
private final AreaManager areas;
private final SpriteManager sprites;
private RSTextureProvider rsTextureProvider;
@@ -93,15 +105,106 @@ public class MapImageDumper
@Setter
private boolean outlineRegions;
public MapImageDumper(Store store)
@Getter
@Setter
private boolean renderMap = true;
@Getter
@Setter
private boolean renderObjects = true;
@Getter
@Setter
private boolean renderIcons = true;
@Getter
@Setter
private boolean transparency = false;
@Getter
@Setter
private boolean lowMemory = true;
public MapImageDumper(Store store, KeyProvider keyProvider)
{
this.store = store;
this.areas = new AreaManager(store);
this.sprites = new SpriteManager(store);
objectManager = new ObjectManager(store);
this(store, new RegionLoader(store, keyProvider));
}
public void load() throws IOException
public MapImageDumper(Store store, RegionLoader regionLoader)
{
this.store = store;
this.regionLoader = regionLoader;
this.areas = new AreaManager(store);
this.sprites = new SpriteManager(store);
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
return 0.5;
}
public MapImageDumper setBrightness(double brightness)
{
colorPalette = JagexColor.createPalette(brightness);
return this;
}
public MapImageDumper load() throws IOException
{
loadUnderlays(store);
loadOverlays(store);
@@ -111,10 +214,12 @@ public class MapImageDumper
textureManager.load();
rsTextureProvider = new RSTextureProvider(textureManager, sprites);
loadRegions(store);
loadRegions();
areas.load();
sprites.load();
loadSprites();
return this;
}
public BufferedImage drawMap(int z)
@@ -131,11 +236,19 @@ 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);
BufferedImage image = new BufferedImage(pixelsX, pixelsY, 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);
@@ -144,15 +257,35 @@ 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;
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);
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;
@@ -160,31 +293,41 @@ public class MapImageDumper
private void drawMap(BufferedImage image, int drawBaseX, int drawBaseY, int z, Region region)
{
int[][] map = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE];
drawMap(map, region, z);
int[][] above = null;
if (z < 3)
if (!renderMap)
{
above = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE];
drawMap(above, region, z + 1);
return;
}
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);
}
}
}
}
@@ -208,15 +351,27 @@ 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)
{
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);
}
}
}
}
@@ -345,11 +500,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;
@@ -361,28 +516,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)
@@ -390,9 +544,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;
}
}
@@ -486,208 +640,221 @@ public class MapImageDumper
private void drawObjects(BufferedImage image, int drawBaseX, int drawBaseY, Region region, int z)
{
Graphics2D graphics = image.createGraphics();
for (Location location : region.getLocations())
if (!renderObjects)
{
return;
}
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)
List<Location> planeLocs = new ArrayList<>();
List<Location> pushDownLocs = new ArrayList<>();
List<List<Location>> layers = Arrays.asList(planeLocs, pushDownLocs);
for (int localX = 0; localX < Region.X; localX++)
{
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 - 1 - 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;
}
if (object.getMapSceneID() != -1)
{
Image spriteImage = scaledMapIcons.get(object.getMapSceneID());
graphics.drawImage(spriteImage, drawX * MAP_SCALE, drawY * MAP_SCALE, null);
}
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<Location> 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;
int rgb = wallColor;
if (object.getWallOrDoor() != 0)
{
rgb = doorColor;
}
rgb |= 0xFF000000;
if (object.getMapSceneID() != -1)
{
blitMapDecoration(image, drawX, drawY, object);
}
else if (drawX >= 0 && drawY >= 0 && drawX < image.getWidth() && drawY < image.getHeight())
{
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;
}
if (drawX >= 0 && drawY >= 0 && drawX < image.getWidth() && drawY < image.getHeight())
{
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 (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;
if (object.getMapSceneID() != -1)
{
blitMapDecoration(image, drawX, drawY, object);
}
}
}
}
}
else if (type == 9)
{
if (object.getMapSceneID() != -1)
{
Image spriteImage = scaledMapIcons.get(object.getMapSceneID());
graphics.drawImage(spriteImage, drawX, drawY, null);
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 = 0xEE_EEEE;
if (hash > 0)
{
rgb = 0xEE_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)
{
Image spriteImage = scaledMapIcons.get(object.getMapSceneID());
graphics.drawImage(spriteImage, drawX, drawY, null);
}
}
}
graphics.dispose();
}
private void drawObjects(BufferedImage image, int z)
@@ -715,7 +882,7 @@ public class MapImageDumper
Graphics2D graphics = image.createGraphics();
drawMapIcons(graphics, region, z, drawBaseX, drawBaseY);
drawMapIcons(image, region, z, drawBaseX, drawBaseY);
if (labelRegions)
{
@@ -854,12 +1021,22 @@ 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)
{
return;
}
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;
@@ -869,9 +1046,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);
@@ -880,27 +1054,26 @@ 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);
}
}
}
private void loadRegions(Store store) throws IOException
private void loadRegions() throws IOException
{
regionLoader = new RegionLoader(store);
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
@@ -953,35 +1126,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);
}
}
}
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.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);
}
}

View File

@@ -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)

View File

@@ -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
Sprite spritePixels = new Sprite(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();

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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<Integer, Region> 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,25 +103,31 @@ public class RegionLoader
Region region = new Region(i);
region.loadTerrain(mapDef);
Integer[] keysTmp = keyManager.getKeys(i);
if (keysTmp != null)
int[] keys = keyProvider.getKey(i);
if (keys != null)
{
int[] keys = {keysTmp[0], keysTmp[1], keysTmp[2], keysTmp[3]};
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())
@@ -154,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;
@@ -173,4 +190,4 @@ public class RegionLoader
{
return highestY;
}
}
}

View File

@@ -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<ImageReader> 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<Callable<ImagePartLoader>> 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<ImagePartLoader>
{
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<ImageReader> 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<FileDataBuffer> 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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2021, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.cache.util;
import lombok.Data;
@Data
public class XteaKey
{
private int region;
private int keys[];
}

View File

@@ -24,25 +24,40 @@
*/
package net.runelite.cache.util;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
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);
private Map<Integer, Integer[]> keys = new HashMap<>();
private final Map<Integer, int[]> keys = new HashMap<>();
public void loadKeys()
public void loadKeys(InputStream in)
{
keys = null;
// CHECKSTYLE:OFF
List<XteaKey> k = new Gson()
.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), new TypeToken<List<XteaKey>>() { }.getType());
// CHECKSTYLE:ON
for (XteaKey key : k)
{
keys.put(key.getRegion(), key.getKeys());
}
logger.info("Loaded {} keys", keys.size());
}
public Integer[] getKeys(int region)
@Override
public int[] getKey(int region)
{
return keys.get(region);
}

View File

@@ -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())
{

View File

@@ -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())
{

View File

@@ -52,7 +52,7 @@ public class HeightMapDumperTest
store.load();
HeightMapDumper dumper = new HeightMapDumper(store);
dumper.load();
dumper.load(null);
BufferedImage image = dumper.drawHeightMap(0);

View File

@@ -65,7 +65,6 @@ public class MapDumperTest
File base = StoreLocation.LOCATION,
outDir = folder.newFolder();
XteaKeyManager keyManager = new XteaKeyManager();
keyManager.loadKeys();
try (Store store = new Store(base))
{
@@ -76,7 +75,7 @@ public class MapDumperTest
for (int i = 0; i < MAX_REGIONS; i++)
{
Integer[] keysTmp = keyManager.getKeys(i);
int[] keys = keyManager.getKey(i);
int x = i >> 8;
int y = i & 0xFF;
@@ -95,9 +94,8 @@ public class MapDumperTest
Files.write(data, new File(outDir, "m" + x + "_" + y + ".dat"));
if (keysTmp != null)
if (keys != null)
{
int[] keys = {keysTmp[0], keysTmp[1], keysTmp[2], keysTmp[3]};
try
{
data = land.decompress(storage.loadArchive(land), keys);
@@ -122,7 +120,6 @@ public class MapDumperTest
Storage storage = store.getStorage();
Index index = store.getIndex(IndexType.MAPS);
XteaKeyManager keyManager = new XteaKeyManager();
keyManager.loadKeys();
for (int i = 0; i < MAX_REGIONS; ++i)
{
@@ -143,10 +140,9 @@ public class MapDumperTest
MapDefinition mapDef = new MapLoader().load(x, y, data);
LocationsDefinition locDef = null;
Integer[] keysTmp = keyManager.getKeys(i);
if (keysTmp != null)
int[] keys = keyManager.getKey(i);
if (keys != null)
{
int[] keys = {keysTmp[0], keysTmp[1], keysTmp[2], keysTmp[3]};
try
{
data = land.decompress(storage.loadArchive(land), keys);

View File

@@ -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;

View File

@@ -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())
{

View File

@@ -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);

View File

@@ -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,

View File

@@ -1246,6 +1246,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
@@ -2111,6 +2122,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
*/

View File

@@ -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}

View File

@@ -747,19 +747,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();
@@ -787,7 +781,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);
}
@@ -798,7 +792,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,

View File

@@ -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;

View File

@@ -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
{
/**

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2022, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.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;
}

View File

@@ -856,6 +856,7 @@ public final class WidgetID
static class LunarSpellBook
{
static final int LUNAR_HOME_TELEPORT = 101;
static final int FERTILE_SOIL = 126;
}
static class ArceuusSpellBook

View File

@@ -507,6 +507,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),

View File

@@ -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;
@@ -90,6 +91,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;
@@ -106,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: <col=ff0000>(\\d+)</col>");
private static final Pattern KILLCOUNT_PATTERN = Pattern.compile("Your (?<pre>completion count for |subdued |completed )?(?<boss>.+?) (?<post>(?:(?:kill|harvest|lap|completion) )?(?:count )?)is: <col=ff0000>(?<kc>\\d+)</col>");
private static final String TEAM_SIZES = "(?<teamsize>\\d+(?:\\+|-\\d+)? players?|Solo)";
private static final Pattern RAIDS_PB_PATTERN = Pattern.compile("<col=ef20ff>Congratulations - your raid is complete!</col><br>Team size: <col=ff0000>" + TEAM_SIZES + "</col> Duration:</col> <col=ff0000>(?<pb>[0-9:]+(?:\\.[0-9]+)?)</col> \\(new personal best\\)</col>");
private static final Pattern RAIDS_DURATION_PATTERN = Pattern.compile("<col=ef20ff>Congratulations - your raid is complete!</col><br>Team size: <col=ff0000>" + TEAM_SIZES + "</col> Duration:</col> <col=ff0000>[0-9:.]+</col> Personal best: </col><col=ff0000>(?<pb>[0-9:]+(?:\\.[0-9]+)?)</col>");
@@ -220,7 +222,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 +305,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 +320,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);
@@ -371,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)
@@ -784,9 +810,6 @@ public class ChatCommandsPlugin extends Plugin
case HOPPING:
pohOwner = null;
break;
case LOGGED_IN:
loadPetIcons();
break;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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")

View File

@@ -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;

View File

@@ -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);

View File

@@ -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),

View File

@@ -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<BarMode, BarRenderer> 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);
}
}

View File

@@ -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();

View File

@@ -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",

View File

@@ -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);

View File

@@ -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<T> 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<T> 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<T> 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<T> 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));
}
}

View File

@@ -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;
}

View File

@@ -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 (?<compostType>ultra|super|)compost\\.");
private static final Pattern FERTILE_SOIL_CAST = Pattern.compile(
"The .+ has been treated with (?<compostType>ultra|super|)compost\\.");
private static final Pattern ALREADY_TREATED = Pattern.compile(
"This .+ has already been (treated|fertilised) with (?<compostType>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 (?<compostType>ultra|super|)compost\\..*");
private static final ImmutableSet<Integer> 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<FarmingPatch, PendingCompost> 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;
}
}

View File

@@ -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()

View File

@@ -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())

View File

@@ -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<Tab, SummaryState> 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;

View File

@@ -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

View File

@@ -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: <col=ff0000>1</col> x <col=ff0000>Kebab</col>.", null, 0);
chatCommandsPlugin.onChatMessage(chatMessage);
verify(configManager, never()).setRSProfileConfiguration(anyString(), anyString(), anyInt());
}
}

View File

@@ -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"));

View File

@@ -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("<gt>:D<lt>");
@@ -119,8 +123,8 @@ public class EmojiPluginTest
@Test
public void testEmojiUpdateMessage()
{
String PARTY_POPPER = "<img=" + (-1 + Emoji.getEmoji("@@@").ordinal()) + '>';
String OPEN_MOUTH = "<img=" + (-1 + Emoji.getEmoji(":O").ordinal()) + '>';
String PARTY_POPPER = "<img=" + Emoji.getEmoji("@@@").ordinal() + '>';
String OPEN_MOUTH = "<img=" + Emoji.getEmoji(":O").ordinal() + '>';
assertNull(emojiPlugin.updateMessage("@@@@@"));
assertEquals(PARTY_POPPER, emojiPlugin.updateMessage("@@@"));
assertEquals(PARTY_POPPER + ' ' + PARTY_POPPER, emojiPlugin.updateMessage("@@@ @@@"));

View File

@@ -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);
}
}

View File

@@ -68,8 +68,7 @@ public abstract class RSItemCompositionMixin implements RSItemComposition
@MethodHook(value = "post", end = true)
public void post()
{
final PostItemComposition event = new PostItemComposition();
event.setItemComposition(this);
final PostItemComposition event = new PostItemComposition(this);
client.getCallbacks().post(event);
}

View File

@@ -2,6 +2,7 @@ package net.runelite.mixins;
import net.runelite.api.IterableHashTable;
import net.runelite.api.Node;
import net.runelite.api.events.PostObjectComposition;
import net.runelite.api.mixins.Copy;
import net.runelite.api.mixins.Inject;
import net.runelite.api.mixins.MethodHook;
@@ -36,6 +37,14 @@ public abstract class RSObjectCompositionMixin implements RSObjectComposition
}
@Inject
@MethodHook(value = "postDecode", end = true)
public void postDecode()
{
final PostObjectComposition event = new PostObjectComposition(this);
client.getCallbacks().post(event);
}
@Inject
@Override
public int getAccessBitMask()

View File

@@ -1366,6 +1366,12 @@ public interface RSClient extends RSGameEngine, Client
@Import("meslayerContinueWidget")
Widget getMessageContinueWidget();
@Import("playingJingle")
boolean isPlayingJingle();
@Import("musicTrackGroupId")
int getMusicCurrentTrackId();
@Import("musicPlayerStatus")
void setMusicPlayerStatus(int var0);
@@ -1579,6 +1585,9 @@ public interface RSClient extends RSGameEngine, Client
@Import("ObjectDefinition_cached")
RSEvictingDualNodeHashTable getObjectDefinitionCache();
@Import("ObjectDefinition_cached")
RSEvictingDualNodeHashTable getObjectCompositionCache();
@Import("ObjectDefinition_cachedModelData")
RSEvictingDualNodeHashTable getObjectDefinitionModelDataCache();

View File

@@ -30,7 +30,8 @@ public final class Client extends GameEngine implements Usernamed, OAuthApi {
)
public static int field779;
@ObfuscatedName("ss")
static boolean field746;
@Export("playingJingle")
static boolean playingJingle;
@ObfuscatedName("tz")
@ObfuscatedGetter(
intValue = 599158567
@@ -1583,7 +1584,7 @@ public final class Client extends GameEngine implements Usernamed, OAuthApi {
destinationY = 0; // L: 571
minimapState = 0; // L: 578
currentTrackGroupId = -1; // L: 579
field746 = false; // L: 580
playingJingle = false; // L: 580
soundEffectCount = 0; // L: 586
soundEffectIds = new int[50]; // L: 587
queuedSoundEffectLoops = new int[50]; // L: 588
@@ -1850,7 +1851,7 @@ public final class Client extends GameEngine implements Usernamed, OAuthApi {
var2 = false; // L: 993
}
if (var2 && field746 && KeyHandler.pcmPlayer0 != null) { // L: 996 997
if (var2 && playingJingle && KeyHandler.pcmPlayer0 != null) { // L: 996 997
KeyHandler.pcmPlayer0.tryDiscard();
}

View File

@@ -131,7 +131,7 @@ public final class ItemLayer {
System.gc(); // L: 2932
Actor.method2183(2); // L: 2933
Client.currentTrackGroupId = -1; // L: 2934
Client.field746 = false; // L: 2935
Client.playingJingle = false; // L: 2935
ClientPreferences.method2259(); // L: 2936
Decimator.updateGameState(10); // L: 2937
} // L: 2938

View File

@@ -143,7 +143,7 @@ public class Players {
static void method2419(int var0, int var1) {
if (class131.clientPreferences.method2321() != 0 && var0 != -1) { // L: 3622
class18.method266(Message.archive11, var0, 0, class131.clientPreferences.method2321(), false); // L: 3623
Client.field746 = true; // L: 3624
Client.playingJingle = true; // L: 3624
}
} // L: 3626

View File

@@ -214,11 +214,11 @@ public class SoundCache {
)
@Export("playSong")
static void playSong(int var0) {
if (var0 == -1 && !Client.field746) { // L: 3608
if (var0 == -1 && !Client.playingJingle) { // L: 3608
class273.midiPcmStream.clear(); // L: 3610
class273.musicPlayerStatus = 1; // L: 3611
class273.musicTrackArchive = null; // L: 3612
} else if (var0 != -1 && var0 != Client.currentTrackGroupId && class131.clientPreferences.method2321() != 0 && !Client.field746) { // L: 3615
} else if (var0 != -1 && var0 != Client.currentTrackGroupId && class131.clientPreferences.method2321() != 0 && !Client.playingJingle) { // L: 3615
NPCComposition.method3530(2, class16.archive6, var0, 0, class131.clientPreferences.method2321(), false); // L: 3616
}

View File

@@ -96,12 +96,12 @@ public class class1 implements Callable {
if (var0 != class131.clientPreferences.method2321()) { // L: 12009
if (class131.clientPreferences.method2321() == 0 && Client.currentTrackGroupId != -1) { // L: 12010
class18.method266(class16.archive6, Client.currentTrackGroupId, 0, var0, false); // L: 12011
Client.field746 = false; // L: 12012
Client.playingJingle = false; // L: 12012
} else if (var0 == 0) { // L: 12014
class273.midiPcmStream.clear(); // L: 12016
class273.musicPlayerStatus = 1; // L: 12017
class273.musicTrackArchive = null; // L: 12018
Client.field746 = false; // L: 12020
Client.playingJingle = false; // L: 12020
} else if (class273.musicPlayerStatus != 0) { // L: 12023
class273.musicTrackVolume = var0;
} else {

View File

@@ -139,12 +139,12 @@ public class class136 extends class144 {
}
}
if (Client.field746 && !class307.method5789()) { // L: 3546
if (Client.playingJingle && !class307.method5789()) { // L: 3546
if (class131.clientPreferences.method2321() != 0 && Client.currentTrackGroupId != -1) { // L: 3547
class18.method266(class16.archive6, Client.currentTrackGroupId, 0, class131.clientPreferences.method2321(), false);
}
Client.field746 = false; // L: 3548
Client.playingJingle = false; // L: 3548
}
} // L: 3550