Created
October 2, 2024 04:07
-
-
Save JabDoesThings/20e092b75b4df7c60c771dd3890df413 to your computer and use it in GitHub Desktop.
Reads & Accesses TexturePack files for Project Zomboid 41.78.16.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.asledgehammer; | |
| import javax.imageio.ImageIO; | |
| import java.awt.image.BufferedImage; | |
| import java.io.*; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| public class TexturePack { | |
| private final HashMap<String, TexturePage> mapPages; | |
| private final ArrayList<TexturePage> pages; | |
| private final HashMap<String, SubTexture> mapSubTextures; | |
| final File file; | |
| private final String name; | |
| final int metaVersion; | |
| private TexturePack(File file, int metaVersion) { | |
| this.file = file; | |
| this.name = file.getName().split("\\.")[0].trim(); | |
| this.metaVersion = metaVersion; | |
| this.mapPages = new HashMap<>(); | |
| this.pages = new ArrayList<>(); | |
| this.mapSubTextures = new HashMap<>(); | |
| } | |
| public static TexturePack read(File file) throws IOException { | |
| int metaVersion; | |
| FileInputStream fis = new FileInputStream(file); | |
| BufferedInputStream bis = new BufferedInputStream(fis); | |
| PositionInputStream pis = new PositionInputStream(bis); | |
| // Skip an int32. | |
| pis.mark(4); | |
| int magicByte1 = pis.read(); | |
| int magicByte2 = pis.read(); | |
| int magicByte3 = pis.read(); | |
| int magicByte4 = pis.read(); | |
| if (magicByte1 == 80 && magicByte2 == 90 && magicByte3 == 80 && magicByte4 == 75) { | |
| metaVersion = pis.readInt(pis); | |
| if (metaVersion != 1) { | |
| throw new IOException("invalid .pack file version: " + metaVersion); | |
| } | |
| } else { | |
| pis.reset(); | |
| metaVersion = 0; | |
| } | |
| TexturePack texturePack = new TexturePack(file, metaVersion); | |
| int pageCount = pis.readInt(pis); | |
| for (int index = 0; index < pageCount; index++) { | |
| TexturePage page = new TexturePage(texturePack, pis); | |
| texturePack.pages.add(page); | |
| texturePack.mapPages.put(page.name, page); | |
| for (SubTextureInfo subTextureInfo : page.subTextureInfo) { | |
| texturePack.mapSubTextures.put(subTextureInfo.name, new SubTexture(page, subTextureInfo)); | |
| } | |
| } | |
| // Close the stream. | |
| pis.close(); | |
| return texturePack; | |
| } | |
| @Override | |
| public String toString() { | |
| return "TexturePack(" | |
| + this.name | |
| + "): Pages(" | |
| + this.pages.size() | |
| + ") SubTextures(" | |
| + mapSubTextures.size() | |
| + ")"; | |
| } | |
| public static void main(String[] args) { | |
| if (args.length == 0) { | |
| System.out.println("[path_to_pack_file]"); | |
| return; | |
| } | |
| // Example: "D:/SteamLibrary/steamapps/common/ProjectZomboid/media/texturepacks/Tiles2x.pack"; | |
| String pathPack = args[0]; | |
| File filePack = new File(pathPack); | |
| try { | |
| TexturePack texturePack = TexturePack.read(filePack); | |
| System.out.println(texturePack); | |
| TexturePage page = texturePack.pages.get(0); | |
| BufferedImage img = page.getImage(); | |
| ImageIO.write(img, "PNG", new File(page.name + ".png")); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| class TexturePage { | |
| final ArrayList<SubTextureInfo> subTextureInfo = new ArrayList<>(); | |
| String name; | |
| long pngBlockAddress; | |
| int pngBlockSize; | |
| boolean hasAlpha; | |
| private BufferedImage image; | |
| TexturePack texturePack; | |
| public TexturePage(TexturePack texturePack, PositionInputStream pis) throws IOException { | |
| this.texturePack = texturePack; | |
| String name = pis.readString(pis); | |
| int subTextureCount = pis.readInt(pis); | |
| boolean hasAlpha = pis.readInt(pis) != 0; | |
| this.name = name; | |
| this.hasAlpha = hasAlpha; | |
| for (int index = 0; index < subTextureCount; index++) { | |
| String texName = pis.readString(pis); | |
| int x = pis.readInt(pis); | |
| int y = pis.readInt(pis); | |
| int width = pis.readInt(pis); | |
| int height = pis.readInt(pis); | |
| int ox = pis.readInt(pis); | |
| int oy = pis.readInt(pis); | |
| int fx = pis.readInt(pis); | |
| int fy = pis.readInt(pis); | |
| this.subTextureInfo.add(new SubTextureInfo(x, y, width, height, ox, oy, fx, fy, texName)); | |
| } | |
| this.pngBlockAddress = pis.getPosition(); | |
| if (texturePack.metaVersion == 0) { | |
| // We pass until the last byte of the PNG data block. | |
| int val; | |
| do { | |
| val = pis.readIntByte(pis); | |
| } while (val != -559038737); | |
| pngBlockSize = (int) ((pis.getPosition() - 1) - pngBlockAddress); | |
| } else { | |
| // We know how many bytes to skip over. | |
| int val = pis.readInt(pis); | |
| pis.skipInput(pis, val); | |
| pngBlockSize = val; | |
| } | |
| } | |
| @Override | |
| public String toString() { | |
| return "Page{" | |
| + "name='" | |
| + name | |
| + '\'' | |
| + ", pngBlockAddress=" | |
| + pngBlockAddress | |
| + ", hasAlpha=" | |
| + hasAlpha | |
| + '}'; | |
| } | |
| private static long skipInput(InputStream is, long length) throws IOException { | |
| long value = 0L; | |
| while (value < length) { | |
| long var5 = is.skip(length - value); | |
| if (var5 > 0L) value += var5; | |
| } | |
| return value; | |
| } | |
| public BufferedImage getImage() throws IOException { | |
| if (image == null) { | |
| FileInputStream fis = new FileInputStream(texturePack.file); | |
| System.out.println("pngBlockAddress: " + pngBlockAddress + ", pngBlockSize: " + pngBlockSize); | |
| skipInput(fis, pngBlockAddress); | |
| if (texturePack.metaVersion >= 1) { | |
| fis.read(); | |
| fis.read(); | |
| fis.read(); | |
| fis.read(); | |
| } | |
| byte[] b = fis.readNBytes(pngBlockSize); | |
| image = ImageIO.read(new ByteArrayInputStream(b)); | |
| } | |
| return image; | |
| } | |
| } | |
| class SubTexture { | |
| final TexturePage page; | |
| final SubTextureInfo info; | |
| SubTexture(TexturePage page, SubTextureInfo subTextureInfo) { | |
| this.page = page; | |
| this.info = subTextureInfo; | |
| } | |
| } | |
| class SubTextureInfo { | |
| public int w; | |
| public int h; | |
| public int x; | |
| public int y; | |
| public int ox; | |
| public int oy; | |
| public int fx; | |
| public int fy; | |
| public String name; | |
| public SubTextureInfo( | |
| int x, int y, int width, int height, int ox, int oy, int fx, int fy, String name) { | |
| this.x = x; | |
| this.y = y; | |
| this.w = width; | |
| this.h = height; | |
| this.ox = ox; | |
| this.oy = oy; | |
| this.fx = fx; | |
| this.fy = fy; | |
| this.name = name; | |
| } | |
| } | |
| class PositionInputStream extends FilterInputStream { | |
| // private int chl1 = 0; | |
| private int chl2 = 0; | |
| private int chl3 = 0; | |
| private int chl4 = 0; | |
| private long pos = 0; | |
| private long mark = 0; | |
| public PositionInputStream(InputStream is) { | |
| super(is); | |
| } | |
| public synchronized long getPosition() { | |
| return this.pos; | |
| } | |
| public synchronized int read() throws IOException { | |
| int val = super.read(); | |
| if (val >= 0) this.pos++; | |
| return val; | |
| } | |
| public synchronized int read(byte[] bytes, int offset, int length) throws IOException { | |
| int value = super.read(bytes, offset, length); | |
| if (value > 0) this.pos += value; | |
| return value; | |
| } | |
| public synchronized long skip(long n) throws IOException { | |
| long value = super.skip(n); | |
| if (value > 0L) this.pos += value; | |
| return value; | |
| } | |
| public synchronized void mark(int limit) { | |
| super.mark(limit); | |
| this.mark = this.pos; | |
| } | |
| public synchronized void reset() throws IOException { | |
| if (!this.markSupported()) { | |
| throw new IOException("Mark not supported."); | |
| } else { | |
| super.reset(); | |
| this.pos = this.mark; | |
| } | |
| } | |
| void skipInput(InputStream is, long length) throws IOException { | |
| long offset = 0; | |
| while (offset < length) { | |
| long var5 = is.skip(length - offset); | |
| if (var5 > 0) offset += var5; | |
| } | |
| } | |
| int readInt(InputStream is) throws IOException { | |
| int byte1 = is.read(); | |
| int byte2 = is.read(); | |
| int byte3 = is.read(); | |
| int byte4 = is.read(); | |
| chl2 = byte2; | |
| chl3 = byte3; | |
| chl4 = byte4; | |
| if ((byte1 | byte2 | byte3 | byte4) < 0) { | |
| throw new EOFException(); | |
| } else { | |
| return byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24); | |
| } | |
| } | |
| int readIntByte(InputStream is) throws IOException { | |
| int byte1 = chl2; | |
| int byte2 = chl3; | |
| int byte3 = chl4; | |
| int byte4 = is.read(); | |
| chl2 = byte2; | |
| chl3 = byte3; | |
| chl4 = byte4; | |
| if ((byte1 | byte2 | byte3 | byte4) < 0) { | |
| throw new EOFException(); | |
| } else { | |
| return byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24); | |
| } | |
| } | |
| String readString(InputStream is) throws IOException { | |
| StringBuilder stringBuilder = new StringBuilder(); | |
| int len = readInt(is); | |
| for (int offset = 0; offset < len; offset++) { | |
| stringBuilder.append((char) is.read()); | |
| } | |
| return stringBuilder.toString(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment