Skip to content

Instantly share code, notes, and snippets.

@JabDoesThings
Created October 2, 2024 04:07
Show Gist options
  • Select an option

  • Save JabDoesThings/20e092b75b4df7c60c771dd3890df413 to your computer and use it in GitHub Desktop.

Select an option

Save JabDoesThings/20e092b75b4df7c60c771dd3890df413 to your computer and use it in GitHub Desktop.
Reads & Accesses TexturePack files for Project Zomboid 41.78.16.
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