diff --git a/src/main/java/entralinked/utility/TiledImageReader.java b/src/main/java/entralinked/utility/TiledImageReader.java index 1370de3..3cfc54b 100644 --- a/src/main/java/entralinked/utility/TiledImageReader.java +++ b/src/main/java/entralinked/utility/TiledImageReader.java @@ -4,6 +4,9 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + /** * Utility class for reading tiled images (C-Gear & Pokédex skin data) into a usable {@link BufferedImage}. */ @@ -15,42 +18,70 @@ public class TiledImageReader { public static final int COLOR_PALETTE_SIZE = 16; public static final int SCREEN_WIDTH = 256; public static final int SCREEN_HEIGHT = 192; - public static final int SCREEN_TILE_COUNT = SCREEN_WIDTH * SCREEN_HEIGHT / TILE_SIZE; + public static final int SCREEN_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT; + public static final int SCREEN_TILE_COUNT = SCREEN_SIZE / TILE_SIZE; + private static final byte[] dexBackgroundColorIndices = new byte[SCREEN_SIZE]; + private static final Logger logger = LogManager.getLogger(); + + static { + // Load Pokédex skin background color indices + try(InputStream inputStream = TiledImageReader.class.getResourceAsStream("/zukan.bin")) { + int currentIndex = 0; + int valueAmount = -1; + int value = -1; + + // Read until end of stream + while((valueAmount = inputStream.read()) != -1 && (value = inputStream.read()) != -1) { + for(int i = 0; i < valueAmount; i++) { + dexBackgroundColorIndices[currentIndex++] = (byte)(value & 63); + } + } + } catch(IOException e) { + logger.error("Could not load Pokédex background data", e); + } + } /** - * Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 255. + * Reads a C-Gear skin from the specified {@link InputStream}. * * @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White. * @return A {@link BufferedImage} representing the read C-Gear skin data. */ public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException { - return readTiledImage(inputStream, 255, normalizeIndices); + return readTiledImage(inputStream, 255, 0, null, normalizeIndices); } /** - * Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 768 and index normalization disabled. + * Reads a Pokédex skin from the specified {@link InputStream}. + * If the skin in question is an overlay, the Pokédex background will be applied to the resulting image automatically. * * @return A {@link BufferedImage} representing the read Pokédex skin data. */ public static BufferedImage readDexSkin(InputStream inputStream) throws IOException { - return readTiledImage(inputStream, 768, false); + return readTiledImage(inputStream, 768, 64, dexBackgroundColorIndices, false); } /** * Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data. * * @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image. + * @param backgroundColorCount The number of background colors this image has. + * @param backgroundColorIndices The background color indices of the background image. If {@code null}, no background will be used. * @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized. * @return A {@link BufferedImage} representing the read image data. */ - public static BufferedImage readTiledImage(InputStream inputStream, int tileCount, boolean normalizeIndices) throws IOException { + public static BufferedImage readTiledImage(InputStream inputStream, + int tileCount, int backgroundColorCount, byte[] backgroundColorIndices, boolean normalizeIndices) throws IOException { BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image int[] tileData = new int[tileCount * TILE_SIZE]; int[] tileIndices = new int[tileCount]; // Tile index lookup table int[] colorPalette = new int[COLOR_PALETTE_SIZE]; + int[] backgroundColorPalette = new int[backgroundColorCount]; + boolean[] transparencyMarkers = new boolean[COLOR_PALETTE_SIZE]; // Keeps track of which colors should become background colors // Read tile data. for(int i = 0; i < tileCount; i++) { + // TODO This next loop is a bottleneck for(int j = 0; j < TILE_SIZE / 2; j++) { int paletteIndices = inputStream.read(); // Contains color palette indices for 2 adjacent pixels. tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1); @@ -61,17 +92,26 @@ public class TiledImageReader { tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i; } - // Read color data. + // Read foreground color data. + for(int i = 0; i < COLOR_PALETTE_SIZE; i++) { + int leftBits = inputStream.read(); + int rightBits = inputStream.read(); + + // So far, the least significant bits of colors that are replaced by background colors are always 0x2A. + // TODO Needs more thorough testing -- is this check reliable? What do the most significant bits signify here? + if(backgroundColorIndices != null && rightBits == 0x2A) { + transparencyMarkers[i] = true; + continue; + } + + colorPalette[i] = convertColor(leftBits | rightBits << 8); + } + + // Read background color data. // Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the // 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex. - for(int i = 0; i < COLOR_PALETTE_SIZE; i++) { - int color = inputStream.read() | inputStream.read() << 8; - - // Convert BGR555 to RGB888 - int red = (color & 0x1F) << 3; - int green = ((color & 0x3E0) >> 5) << 3; - int blue = ((color & 0x7C00) >> 10) << 3; - colorPalette[i] = (red << 16) | (green << 8) | blue; + for(int i = 0; i < backgroundColorCount; i++) { + backgroundColorPalette[i] = convertColor(inputStream.read() | inputStream.read() << 8); } // Map tiles to the resulting image. @@ -107,10 +147,17 @@ public class TiledImageReader { case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically default -> j; // Don't flip }; - - // Finally, set the pixel! + + int pixelX = x + j % TILE_WIDTH; + int pixelY = y + j / TILE_WIDTH; int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex]; - image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[paletteIndex]); + + // If this color has been marked as a 'transparent' color, use the color from the background instead. + int color = !transparencyMarkers[paletteIndex] ? colorPalette[paletteIndex] + : backgroundColorPalette[backgroundColorIndices[pixelY * SCREEN_WIDTH + pixelX]]; + + // Finally, set the pixel! + image.setRGB(pixelX, pixelY, color); } } } else { @@ -120,11 +167,28 @@ public class TiledImageReader { int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH; for(int j = 0; j < TILE_SIZE; j++) { - image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[tileData[i * TILE_SIZE + j]]); + int pixelX = x + j % TILE_WIDTH; + int pixelY = y + j / TILE_WIDTH; + int paletteIndex = tileData[i * TILE_SIZE + j]; + int color = !transparencyMarkers[paletteIndex] ? colorPalette[paletteIndex] + : backgroundColorPalette[backgroundColorIndices[pixelY * SCREEN_WIDTH + pixelX]]; + image.setRGB(pixelX, pixelY, color); } } } return image; } + + /** + * Converts the input BGR555 color value to an RGB888 color value. + * + * @return The RGB888 color value. + */ + private static int convertColor(int color) { + int red = (color & 0x1F) << 3; + int green = ((color & 0x3E0) >> 5) << 3; + int blue = ((color & 0x7C00) >> 10) << 3; + return (red << 16) | (green << 8) | blue; + } } diff --git a/src/main/resources/zukan.bin b/src/main/resources/zukan.bin new file mode 100644 index 0000000..4ec4d4e Binary files /dev/null and b/src/main/resources/zukan.bin differ