Created Dc6 codec

Created Dc6 file codec and Dc6Decoder and tests
Amended Dc to remove Dc#dirOffsets and use virtual method instead
Dc6 implements offsets for each frame and therefore requires a bit of specialization
This commit is contained in:
Collin Smith
2021-11-17 22:48:05 -08:00
parent 46aea7d6a1
commit e39979faa8
6 changed files with 539 additions and 15 deletions

View File

@ -19,22 +19,20 @@ public abstract class Dc<D extends Dc.Direction>
protected final FileHandle handle;
protected final int numDirections;
protected final int numFrames;
protected final int[] dirOffsets;
protected final D[] directions;
@SuppressWarnings("unchecked")
protected Dc(FileHandle handle, int numDirections, int numFrames, int[] dirOffsets, Class<D> dirType) {
protected Dc(FileHandle handle, int numDirections, int numFrames, Class<D> dirType) {
this.handle = handle;
this.numDirections = numDirections;
this.numFrames = numFrames;
this.dirOffsets = dirOffsets;
this.directions = (D[]) Array.newInstance(dirType, numDirections);
}
public Dc<D> read(ByteBuf buffer, int direction) {
assert directions[direction] == null;
assert buffer.isReadable(dirOffsets[direction + 1] - dirOffsets[direction])
: handle + " buffer.isReadable(" + (dirOffsets[direction + 1] - dirOffsets[direction]) + ") = " + buffer.readableBytes();
assert buffer.isReadable(dirOffset(direction + 1) - dirOffset(direction))
: handle + " buffer.isReadable(" + (dirOffset(direction + 1) - dirOffset(direction)) + ") = " + buffer.readableBytes();
retain(); // increment refCnt for each direction read
return this;
}
@ -70,14 +68,7 @@ public abstract class Dc<D extends Dc.Direction>
return numFrames;
}
/** Dc file direction offsets table */
public int[] dirOffsets() {
return dirOffsets;
}
public int dirOffset(int d) {
return dirOffsets[d];
}
public abstract int dirOffset(int d);
public D direction(int d) {
return directions[d];

View File

@ -0,0 +1,259 @@
package com.riiablo.file;
import io.netty.buffer.ByteBuf;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.SwappedDataInputStream;
import org.apache.commons.lang3.exception.ExceptionUtils;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.riiablo.codec.util.BBox;
import com.riiablo.io.ByteInput;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;
import com.riiablo.logger.MDC;
import com.riiablo.util.DebugUtils;
public class Dc6 extends Dc<Dc6.Dc6Direction> {
private static final Logger log = LogManager.getLogger(Dc6.class);
@SuppressWarnings("GDXJavaStaticResource")
public static Texture MISSING_TEXTURE;
//final FileHandle handle; // Dc#handle
final byte[] signature;
final int version;
final int format;
final byte[] section;
//final int numDirections; // Dc#numDirections
//final int numFrames; // Dc#numFrames
final int[] frameOffsets;
public static Dc6 read(FileHandle handle, InputStream stream) {
SwappedDataInputStream in = new SwappedDataInputStream(stream);
try {
return read(handle, in);
} catch (Throwable t) {
return ExceptionUtils.rethrow(t);
}
}
public static Dc6 read(FileHandle handle, SwappedDataInputStream in) throws IOException {
byte[] signature = IOUtils.readFully(in, 4);
int version = in.readInt();
int format = in.readInt();
byte[] section = IOUtils.readFully(in, 4);
int numDirections = in.readInt();
int numFrames = in.readInt();
log.trace("signature: {}", DebugUtils.toByteArray(signature));
log.trace("version: {}", version);
log.trace("format: {}", format);
log.trace("section: {}", DebugUtils.toByteArray(section));
log.trace("numDirections: {}", numDirections);
log.trace("numFrames: {}", numFrames);
final int totalFrames = numDirections * numFrames;
final int[] frameOffsets = new int[totalFrames + 1];
for (int i = 0, s = totalFrames; i < s; i++) frameOffsets[i] = in.readInt();
frameOffsets[totalFrames] = (int) handle.length();
log.trace("frameOffsets: {}", frameOffsets);
return new Dc6(handle, signature, version, format, section, numDirections, numFrames, frameOffsets);
}
Dc6(
FileHandle handle,
byte[] signature,
int version,
int format,
byte[] section,
int numDirections,
int numFrames,
int[] frameOffsets
) {
super(handle, numDirections, numFrames, Dc6Direction.class);
this.signature = signature;
this.version = version;
this.format = format;
this.section = section;
this.frameOffsets = frameOffsets;
}
@Override
public void dispose() {
super.dispose();
}
@Override
public int dirOffset(int d) {
return frameOffsets[d * numFrames];
}
public int frameOffset(int d, int f) {
return frameOffsets[d * numFrames + f];
}
@Override
public Dc6 read(ByteBuf buffer, int direction) {
super.read(buffer, direction);
ByteInput in = ByteInput.wrap(buffer);
directions[direction] = new Dc6Direction(this, direction, in);
return this;
}
public void uploadTextures(int d) {
final Dc6Direction direction = directions[d];
final Dc6Frame[] frame = direction.frames;
final Pixmap[] pixmap = direction.pixmap;
final Texture[] texture = direction.texture;
for (int f = 0; f < numFrames; f++) {
Texture t = texture[f] = new Texture(pixmap[f]);
frame[f].texture.setRegion(t);
pixmap[f].dispose();
pixmap[f] = null;
}
}
public static final class Dc6Direction extends Dc.Direction<Dc6Frame> {
// Dc
final Dc6Frame[] frames;
final BBox box;
final Pixmap[] pixmap;
final Texture[] texture;
Dc6Direction(Dc6 dc6, int d, ByteInput in) {
final int numFrames = dc6.numFrames;
box = new BBox().prepare();
pixmap = new Pixmap[numFrames];
texture = new Texture[numFrames];
Dc6Frame[] frames = this.frames = new Dc6Frame[numFrames];
for (int frame = 0; frame < numFrames; frame++) {
try {
MDC.put("frame", frame);
int offset = dc6.frameOffset(d, frame);
int nextOffset = dc6.frameOffset(d, frame + 1);
log.tracef("nextOffset - offset: 0x%x", nextOffset - offset);
Dc6Frame f = frames[frame] = new Dc6Frame(in.readSlice(nextOffset - offset));
box.max(f.box);
} finally {
MDC.remove("frame");
}
}
}
@Override
public void dispose() {
log.trace("disposing dcc pixmaps");
for (int i = 0, s = pixmap.length; i < s; i++) {
if (pixmap[i] == null) continue;
pixmap[i].dispose();
pixmap[i] = null;
}
log.trace("disposing dcc textures");
for (int i = 0, s = texture.length; i < s; i++) {
if (texture[i] == null) continue;
texture[i].dispose();
texture[i] = null;
}
}
@Override
public Dc6Frame[] frames() {
return frames;
}
@Override
public Dc6Frame frame(int f) {
return frames[f];
}
@Override
public BBox box() {
return box;
}
}
public static final class Dc6Frame extends Dc.Frame {
// Dc
final boolean flipY;
final int width;
final int height;
final int xOffset;
final int yOffset;
final BBox box;
final TextureRegion texture;
// Dc6
final int unk0; // unused
final int nextOffset; // file offset of next frame, unused (header value used in preference)
final int length; // unused
final ByteInput in;
Dc6Frame(ByteInput in) {
this.in = in;
flipY = in.read32() != 0;
width = in.readSafe32u();
height = in.readSafe32u();
xOffset = in.read32();
yOffset = in.read32();
unk0 = in.readSafe32u();
nextOffset = in.readSafe32u();
length = in.readSafe32u();
box = new BBox().asBox(xOffset, flipY ? yOffset : yOffset - height, width, height);
texture = MISSING_TEXTURE == null ? new TextureRegion() : new TextureRegion(MISSING_TEXTURE);
log.trace("flipY: {}", flipY);
log.trace("width: {}", width);
log.trace("height: {}", height);
log.trace("xOffset: {}", xOffset);
log.trace("yOffset: {}", yOffset);
log.tracef("unk0: 0x%x", unk0);
log.tracef("nextOffset: 0x%x", nextOffset);
log.tracef("length: 0x%x", length);
}
@Override
public boolean flipY() {
return flipY;
}
@Override
public int width() {
return width;
}
@Override
public int height() {
return height;
}
@Override
public int xOffset() {
return xOffset;
}
@Override
public int yOffset() {
return yOffset;
}
@Override
public BBox box() {
return box;
}
@Override
public TextureRegion texture() {
return texture;
}
}
}

View File

@ -0,0 +1,75 @@
package com.riiablo.file;
import io.netty.util.internal.PlatformDependent;
import java.util.Arrays;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.utils.BufferUtils;
import com.riiablo.codec.util.BBox;
import com.riiablo.file.Dc6.Dc6Direction;
import com.riiablo.file.Dc6.Dc6Frame;
import com.riiablo.graphics.PaletteIndexedPixmap;
import com.riiablo.io.ByteInput;
public class Dc6Decoder {
static final boolean DEBUG = !true;
static final int MAX_WIDTH = 256;
static final int MAX_HEIGHT = 256;
final byte[] bmp = PlatformDependent.allocateUninitializedArray(MAX_WIDTH * MAX_HEIGHT); // 256x256 px
public void decode(Dc6 dc6, int d) {
decode(dc6, d, dc6.directions[d]);
}
void decode(Dc6 dc6, int d, Dc6Direction dir) {
decodeFrames(dir, dc6.numFrames);
}
void decodeFrames(Dc6Direction dir, int numFrames) {
final Dc6Frame[] frames = dir.frames;
for (int f = 0, s = numFrames; f < s; f++) {
decodeFrame(dir, frames[f], f);
final BBox box = frames[f].box;
final Pixmap[] pixmap = dir.pixmap;
final Pixmap p = pixmap[f] = new PaletteIndexedPixmap(box.width, box.height);
BufferUtils.copy(bmp, 0, p.getPixels().rewind(), box.width * box.height);
}
}
void decodeFrame(Dc6Direction dir, Dc6Frame frame, int f) {
final ByteInput in = frame.in;
final int width = frame.width;
final int height = frame.height;
int x = 0;
int y = height - 1;
int rawIndex = 0;
for (final int s = frame.length; rawIndex < s;) {
int chunkSize = in.read8u();
rawIndex++;
if (chunkSize == 0x80) {
// eol
Arrays.fill(bmp, y * width + x, (y + 1) * width, (byte) 0);
x = 0;
y--;
} else if ((chunkSize & 0x80) != 0) {
// number of transparent pixels
final int length = (chunkSize & 0x7f);
Arrays.fill(bmp, y * width + x, y * width + x + length, (byte) 0);
x += length;
} else {
// number of colors to read
assert chunkSize + x <= width : "chunkSize(" + chunkSize + ") + x(" + x + ") > width(" + width + ")";
in.readBytes(bmp, y * width + x, chunkSize);
rawIndex += chunkSize;
x += chunkSize;
}
}
assert rawIndex == frame.length : "rawIndex(" + rawIndex + ") != frame.length(" + frame.length + ")";
}
}

View File

@ -45,7 +45,7 @@ public final class Dcc extends Dc<Dcc.DccDirection> {
final byte version;
//final int numDirections; // Dc#numDirections
//final int numFrames; // Dc#numFrames
//final int[] dirOffsets; // Dc#dirOffsets;
final int[] dirOffsets;
public static Dcc read(FileHandle handle, InputStream stream) {
SwappedDataInputStream in = new SwappedDataInputStream(stream);
@ -80,9 +80,10 @@ public final class Dcc extends Dc<Dcc.DccDirection> {
int numFrames,
int[] dirOffsets
) {
super(handle, numDirections, numFrames, dirOffsets, DccDirection.class);
super(handle, numDirections, numFrames, DccDirection.class);
this.signature = signature;
this.version = version;
this.dirOffsets = dirOffsets;
}
@Override
@ -90,6 +91,11 @@ public final class Dcc extends Dc<Dcc.DccDirection> {
super.dispose();
}
@Override
public int dirOffset(int d) {
return dirOffsets[d];
}
@Override
public Dcc read(ByteBuf buffer, int direction) {
super.read(buffer, direction);

View File

@ -0,0 +1,145 @@
package com.riiablo.file;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;
import java.io.InputStream;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.riiablo.RiiabloTest;
import com.riiablo.asset.AssetDesc;
import com.riiablo.asset.AssetUtils;
import com.riiablo.asset.param.DcParams;
import com.riiablo.asset.param.MpqParams;
import com.riiablo.codec.Palette;
import com.riiablo.codec.util.BBox;
import com.riiablo.graphics.BlendMode;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.mpq_bytebuf.MpqFileHandle;
import com.riiablo.mpq_bytebuf.MpqFileResolver;
import com.riiablo.util.InstallationFinder;
public class Dc6DecoderTest {
@BeforeEach
public void beforeEach() {
RiiabloTest.clearGdxContext();
}
@ParameterizedTest
@ValueSource(strings = {
"data\\global\\monsters\\ty\\ra\\tyralitnuhth.dc6",
})
void draw_pixmaps(String dc6Name) throws Exception {
FileHandle testHome = InstallationFinder.getInstance().defaultHomeDir();
Dc6Decoder decoder = new Dc6Decoder();
EventExecutor executor = ImmediateEventExecutor.INSTANCE;
AssetDesc<Dc6> parent = AssetDesc.of(dc6Name, Dc6.class, DcParams.of(-1));
MpqFileResolver resolver = new MpqFileResolver(testHome);
MpqFileHandle dc6Handle = resolver.resolve(parent);
InputStream stream = dc6Handle.bufferStream(executor, dc6Handle.sectorSize()).get();
Dc6 dc6 = Dc6.read(dc6Handle, stream);
int offset = dc6.dirOffset(0);
int nextOffset = dc6.dirOffset(1);
ByteBuf buffer = dc6Handle.bufferAsync(executor, offset, nextOffset - offset).get();
dc6.read(buffer, 0);
final Promise<?> promise = executor.newPromise();
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration() {{
title = dc6Name;
forceExit = false;
}};
ApplicationListener listener = new ApplicationAdapter() {
PaletteIndexedBatch batch;
ShaderProgram shader;
Texture paletteTexture;
int frame = 0;
float updater = 0f;
@Override
public void create() {
try {
create0();
} catch (Throwable t) {
t.printStackTrace(System.err);
Gdx.app.exit();
}
}
void create0() {
decoder.decode(dc6, 0);
dc6.uploadTextures(0);
String paletteName = "data\\global\\palette\\ACT1\\pal.dat";
AssetDesc<Palette> paletteDesc = AssetDesc.of(paletteName, Palette.class, MpqParams.of());
MpqFileHandle paletteHandle = resolver.resolve(paletteDesc);
Palette palette = Palette.loadFromStream(paletteHandle.stream());
paletteTexture = palette.render();
ShaderProgram.pedantic = false;
shader = new ShaderProgram(
Gdx.files.internal("shaders/indexpalette3.vert"),
Gdx.files.internal("shaders/indexpalette3.frag"));
if (!shader.isCompiled()) {
throw new GdxRuntimeException("Error compiling shader: " + shader.getLog());
}
batch = new PaletteIndexedBatch(1024, shader);
batch.setGamma(1.2f);
}
@Override
public void render() {
// Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1);
Gdx.gl.glClearColor(1f, 1f, 1f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.setBlendMode(BlendMode.NONE);
batch.begin(paletteTexture);
updater += Gdx.graphics.getDeltaTime();
final float fdelay = 0.10f;
if (updater > fdelay) {
updater -= fdelay;
frame++;
if (frame >= dc6.numFrames) {
frame = 0;
}
}
Dc6.Dc6Direction dir = dc6.directions[0];
BBox box = dir.frames[frame].box;
batch.draw(dir.texture[frame],
dir.box.width + box.xMin, -box.yMax,
box.width, box.height);
batch.end();
}
@Override
public void dispose() {
// also releases dccHandle reference
// release twice, once for header, again for direction
ReferenceCountUtil.release(dc6);
ReferenceCountUtil.release(dc6);
AssetUtils.dispose(paletteTexture);
AssetUtils.dispose(shader);
AssetUtils.dispose(batch);
promise.setSuccess(null);
}
};
new LwjglApplication(listener, config);
promise.awaitUninterruptibly();
resolver.dispose();
}
}

View File

@ -0,0 +1,48 @@
package com.riiablo.file;
import org.junit.jupiter.api.*;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.ImmediateEventExecutor;
import java.io.InputStream;
import com.riiablo.RiiabloTest;
import com.riiablo.asset.AssetDesc;
import com.riiablo.asset.param.DcParams;
import com.riiablo.logger.Level;
import com.riiablo.logger.LogManager;
import com.riiablo.mpq_bytebuf.MpqFileHandle;
import com.riiablo.mpq_bytebuf.MpqFileResolver;
public class Dc6Test extends RiiabloTest {
@BeforeAll
public static void before() {
LogManager.setLevel("com.riiablo.file.Dc6", Level.TRACE);
}
@Test
@DisplayName("dc6_buffers w/ data\\global\\monsters\\ty\\ra\\tyralitnuhth.dc6")
void dc6_buffers() throws Exception {
EventExecutor executor = ImmediateEventExecutor.INSTANCE;
MpqFileResolver resolver = new MpqFileResolver();
try {
final String dc6Name = "data\\global\\monsters\\ty\\ra\\tyralitnuhth.dc6";
AssetDesc<Dc6> parent = AssetDesc.of(dc6Name, Dc6.class, DcParams.of(-1));
MpqFileHandle dc6Handle = resolver.resolve(parent);
try {
InputStream stream = dc6Handle.bufferStream(executor, dc6Handle.sectorSize()).get();
Dc6 dc6 = Dc6.read(dc6Handle, stream);
int offset = dc6.dirOffset(0);
int nextOffset = dc6.dirOffset(1);
ByteBuf buffer = dc6Handle.bufferAsync(executor, offset, nextOffset - offset).get();
dc6.read(buffer, 0);
} finally {
ReferenceCountUtil.release(dc6Handle);
}
} finally {
resolver.dispose();
}
}
}