Added support for DcParams#combineFrames

Added boolean combineFrames argument to Dc#uploadTextures()
Created Dc#numPages() to support combined frames
Defined Dc6#PAGE_SIZE and changed constants to reference it
Implemented combined frames into Dc6 (Dcc unsupported)
Refactored subclass fields into Dc#MISSING_TEXTURE
Created Dc#box() methods
Improved DccLoader test cases with asset failure to load test case
This commit is contained in:
Collin Smith 2021-12-02 22:26:20 -08:00
parent 070295a25c
commit ffad38ad76
11 changed files with 448 additions and 25 deletions

View File

@ -112,7 +112,7 @@ public class DccLoader extends AssetLoader<Dcc> {
log.traceEntry("loadSync(assets: {}, asset: {}, dcc: {})", assets, asset, dcc);
DcParams params = asset.params(DcParams.class);
if (params.direction < 0) return dcc;
dcc.uploadTextures(params.direction);
dcc.uploadTextures(params.direction, params.combineFrames);
return dcc;
}
}

View File

@ -7,15 +7,25 @@ import io.netty.util.ReferenceCounted;
import java.lang.reflect.Array;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Disposable;
import com.riiablo.codec.util.BBox;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;
import static com.riiablo.util.ImplUtils.todo;
public abstract class Dc<D extends Dc.Direction>
extends AbstractReferenceCounted
implements Disposable
{
private static final Logger log = LogManager.getLogger(Dc.class);
@SuppressWarnings("GDXJavaStaticResource")
public static Texture MISSING_TEXTURE;
protected final FileHandle handle;
protected final int numDirections;
protected final int numFrames;
@ -74,6 +84,32 @@ public abstract class Dc<D extends Dc.Direction>
return directions[d];
}
public void uploadTextures(int d, boolean combineFrames) {}
public BBox box() {
return todo();
}
public BBox box(int d) {
return direction(d).box();
}
public BBox box(int d, int f) {
return direction(d).frame(f).box();
}
public int numPages() {
return numFrames;
}
public TextureRegion page(int i) {
return page(0, i);
}
public TextureRegion page(int d, int i) {
return direction(d).frame(i).texture();
}
public static abstract class Direction<F extends Dc.Frame> implements Disposable {
@Override public void dispose() {}
public abstract F[] frames();
@ -90,4 +126,107 @@ public abstract class Dc<D extends Dc.Direction>
public abstract BBox box();
public abstract TextureRegion texture();
}
public static int toRealDir(int d, int numDirs) {
switch (numDirs) {
case 1:
case 2:
case 4: return d;
case 8: return toRealDir8(d);
case 16: return toRealDir16(d);
case 32: return toRealDir32(d);
default: // FIXME: see #113
log.error("numDirs({}) invalid: should be one of {1,2,4,8,16,32}", numDirs);
return 0;
}
}
public static int toRealDir8(int d) {
switch (d) {
case 0: return 1;
case 1: return 3;
case 2: return 5;
case 3: return 7;
case 4: return 0;
case 5: return 2;
case 6: return 4;
case 7: return 6;
default: // FIXME: see #113
log.error("d({}) invalid: should be in [{}..{})", d, 0, 8);
return 0;
}
}
public static int toRealDir16(int d) {
switch (d) {
case 0: return 2;
case 1: return 6;
case 2: return 10;
case 3: return 14;
case 4: return 0;
case 5: return 4;
case 6: return 8;
case 7: return 12;
case 8: return 1;
case 9: return 3;
case 10: return 5;
case 11: return 7;
case 12: return 9;
case 13: return 11;
case 14: return 13;
case 15: return 15;
default: // FIXME: see #113
log.error("d({}) invalid: should be in [{}..{})", d, 0, 16);
return 0;
}
}
public static int toRealDir32(int d) {
switch (d) {
case 0: return 4;
case 1: return 12;
case 2: return 20;
case 3: return 28;
case 4: return 0;
case 5: return 8;
case 6: return 16;
case 7: return 24;
case 8: return 2;
case 9: return 6;
case 10: return 10;
case 11: return 14;
case 12: return 18;
case 13: return 22;
case 14: return 26;
case 15: return 30;
case 16: return 1;
case 17: return 3;
case 18: return 5;
case 19: return 7;
case 20: return 9;
case 21: return 11;
case 22: return 13;
case 23: return 15;
case 24: return 17;
case 25: return 19;
case 26: return 21;
case 27: return 23;
case 28: return 25;
case 29: return 27;
case 30: return 29;
case 31: return 31;
default: // FIXME: see #113
log.error("d({}) invalid: should be in [{}..{})", d, 0, 32);
return 0;
}
}
}

View File

@ -19,11 +19,12 @@ import com.riiablo.logger.Logger;
import com.riiablo.logger.MDC;
import com.riiablo.util.DebugUtils;
import static com.riiablo.graphics.PaletteIndexedPixmap.INDEXED;
public class Dc6 extends Dc<Dc6.Dc6Direction> {
private static final Logger log = LogManager.getLogger(Dc6.class);
@SuppressWarnings("GDXJavaStaticResource")
public static Texture MISSING_TEXTURE;
public static final int PAGE_SIZE = 256;
//final FileHandle handle; // Dc#handle
final byte[] signature;
@ -33,6 +34,7 @@ public class Dc6 extends Dc<Dc6.Dc6Direction> {
//final int numDirections; // Dc#numDirections
//final int numFrames; // Dc#numFrames
final int[] frameOffsets;
int numPages;
public static Dc6 read(FileHandle handle, InputStream stream) {
SwappedDataInputStream in = new SwappedDataInputStream(stream);
@ -83,6 +85,7 @@ public class Dc6 extends Dc<Dc6.Dc6Direction> {
this.format = format;
this.section = section;
this.frameOffsets = frameOffsets;
numPages = numFrames;
}
@Override
@ -90,6 +93,11 @@ public class Dc6 extends Dc<Dc6.Dc6Direction> {
super.dispose();
}
@Override
public int numPages() {
return numPages;
}
@Override
public int dirOffset(int d) {
return frameOffsets[d * numFrames];
@ -107,17 +115,49 @@ public class Dc6 extends Dc<Dc6.Dc6Direction> {
return this;
}
public void uploadTextures(int d) {
@Override
public void uploadTextures(int d, boolean combineFrames) {
final Dc6Direction direction = directions[d];
final Dc6Frame[] frame = direction.frames;
final Pixmap[] pixmap = direction.pixmap;
final Texture[] texture = direction.texture;
if (!combineFrames) {
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;
}
} else {
int rows = 0, columns = 0;
int width = 0, height = 0;
for (int w = 0, s = numFrames; w < s; w++) {
columns++;
width += frame[w].width;
if (frame[w].width < PAGE_SIZE) break;
}
for (int h = 0, s = numFrames; h < s; h += columns) {
rows++;
height += frame[h].height;
if (frame[h].height < PAGE_SIZE) break;
}
numPages = numFrames / (rows * columns);
for (int p = 0, s = numPages, f = 0; p < s; p++) {
int x = 0, y = 0;
Texture t = texture[p] = new Texture(width, height, INDEXED);
frame[p].texture.setRegion(t);
for (int r = 0; r < rows; r++, x = 0) {
for (int c = 0; c < columns; c++, f++) {
t.draw(pixmap[f], x, y);
pixmap[f].dispose();
pixmap[f] = null;
x += PAGE_SIZE;
}
y += PAGE_SIZE;
}
}
}
}
public static final class Dc6Direction extends Dc.Direction<Dc6Frame> {

View File

@ -14,8 +14,8 @@ 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;
static final int MAX_WIDTH = Dc6.PAGE_SIZE;
static final int MAX_HEIGHT = Dc6.PAGE_SIZE;
final byte[] bmp = PlatformDependent.allocateUninitializedArray(MAX_WIDTH * MAX_HEIGHT); // 256x256 px

View File

@ -24,9 +24,6 @@ import com.riiablo.logger.MDC;
public final class Dcc extends Dc<Dcc.DccDirection> {
private static final Logger log = LogManager.getLogger(Dcc.class);
@SuppressWarnings("GDXJavaStaticResource")
public static Texture MISSING_TEXTURE;
static final int ENCODED_BITS[] = {
0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 26, 28, 30, 32
};
@ -102,6 +99,11 @@ public final class Dcc extends Dc<Dcc.DccDirection> {
return dirOffsets[d];
}
@Override
public BBox box(int d, int f) {
return box(d);
}
@Override
public Dcc read(ByteBuf buffer, int direction) {
super.read(buffer, direction);
@ -110,7 +112,9 @@ public final class Dcc extends Dc<Dcc.DccDirection> {
return this;
}
public void uploadTextures(int d) {
@Override
public void uploadTextures(int d, boolean combineFrames) {
if (combineFrames) throw new UnsupportedOperationException("DCC do not support combined frames");
final DccDirection direction = directions[d];
final DccFrame[] frame = direction.frames;
final Pixmap[] pixmap = direction.pixmap;

View File

@ -139,11 +139,11 @@ public class AssetManagerTest extends RiiabloTest {
@ParameterizedTest
@ValueSource(strings = {
"data\\global\\chars\\ba\\hd\\bahdbhma11hs.dcc",
// "data\\global\\CHARS\\BA\\LG\\BALGLITTNHTH.DCC",
"data\\global\\chars\\ba\\lg\\balglittnhth.dcc",
})
void load_mpq(String path) {
AssetDesc<Dcc> asset = AssetDesc.of(path, Dcc.class, DcParams.of(0));
Future<Dcc> handle = assets.load(asset);
Future<? extends Dcc> handle = assets.load(asset);
try {
assertNotNull(handle);
assets.sync(100);

View File

@ -0,0 +1,172 @@
package com.riiablo.asset.loader;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;
import org.apache.commons.lang3.math.NumberUtils;
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.Riiablo;
import com.riiablo.RiiabloTest;
import com.riiablo.asset.AssetDesc;
import com.riiablo.asset.AssetManager;
import com.riiablo.asset.AssetUtils;
import com.riiablo.asset.adapter.GdxFileHandleAdapter;
import com.riiablo.asset.adapter.MpqFileHandleAdapter;
import com.riiablo.asset.param.DcParams;
import com.riiablo.asset.param.MpqParams;
import com.riiablo.asset.resolver.GdxFileHandleResolver;
import com.riiablo.codec.util.BBox;
import com.riiablo.file.Dc;
import com.riiablo.file.Dc6;
import com.riiablo.file.Palette;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.mpq_bytebuf.MpqFileHandle;
import com.riiablo.mpq_bytebuf.MpqFileResolver;
import com.riiablo.util.InstallationFinder;
import com.riiablo.util.InstallationFinder.DefaultNotFound;
import static com.riiablo.graphics.BlendMode.NONE;
import static com.riiablo.graphics.PaletteIndexedPixmap.INDEXED;
public class Dc6LoaderTest {
AssetManager assets;
@BeforeEach
public void beforeEach() throws DefaultNotFound {
RiiabloTest.clearGdxContext();
final InstallationFinder finder = InstallationFinder.getInstance();
Riiablo.home = finder.defaultHomeDir();
assets = new AssetManager()
.resolver(GdxFileHandleResolver.Internal, 0)
.resolver(new MpqFileResolver(), 1)
.paramResolver(Dc.class, DcParams.class)
.adapter(FileHandle.class, new GdxFileHandleAdapter())
.adapter(MpqFileHandle.class, new MpqFileHandleAdapter())
.loader(Dc6.class, new Dc6Loader())
.loader(Palette.class, new PaletteLoader())
;
}
@AfterEach
public void afterEach() {
AssetUtils.dispose(assets);
}
@ParameterizedTest
@CsvSource(value = {
"data\\global\\monsters\\ty\\ra\\tyralitnuhth.dc6,0,false",
"data\\global\\ui\\panel\\invchar6.dc6,0,true",
}, delimiter = ',')
void load(String dccName, String szDirection, String szCombineFrames) throws Throwable {
int direction = NumberUtils.toInt(szDirection);
boolean combineFrames = Boolean.parseBoolean(szCombineFrames);
AssetDesc<Dc6> asset = AssetDesc.of(dccName, Dc6.class, DcParams.of(direction, combineFrames));
EventExecutor executor = ImmediateEventExecutor.INSTANCE;
final Promise<Throwable> promise = executor.newPromise();
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration() {{
title = dccName;
forceExit = false;
}};
ApplicationListener listener = new ApplicationAdapter() {
Throwable throwable;
PaletteIndexedBatch batch;
ShaderProgram shader;
int frame = 0;
float updater = 0f;
Dc6 dc6;
Palette palette;
AssetDesc<Palette> paletteAsset;
@Override
public void create() {
try {
create0();
} catch (Throwable t) {
t.printStackTrace(System.err);
throwable = t;
Gdx.app.exit();
}
}
public void create0() throws InterruptedException {
Dc.MISSING_TEXTURE = new Texture(0, 0, INDEXED);
assets.load(asset)
.addListener((FutureListener<Dc6>) future -> dc6 = future.getNow());
String paletteName = "data\\global\\palette\\ACT1\\pal.dat";
paletteAsset = AssetDesc.of(paletteName, Palette.class, MpqParams.of());
assets.load(paletteAsset)
.addListener((FutureListener<Palette>) future -> palette = future.getNow());
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);
assets.awaitAll(asset, paletteAsset);
}
@Override
public void render() {
Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.setBlendMode(NONE);
batch.begin(palette.texture());
if (combineFrames) {
batch.draw(dc6.page(0), 0, 0);
batch.end();
return;
}
updater += Gdx.graphics.getDeltaTime();
if (updater > 0.25f) {
updater -= 0.25f;
frame++;
if (frame >= dc6.numFrames()) {
frame = 0;
}
}
Dc6.Dc6Direction dir = dc6.direction(0);
BBox box = dir.frame(frame).box();
batch.draw(dc6.direction(0).frame(frame).texture(),
dir.box().width + box.xMin, -box.yMax,
box.width, box.height);
batch.end();
}
@Override
public void dispose() {
assets.unload(paletteAsset);
assets.unload(asset);
AssetUtils.dispose(Dc.MISSING_TEXTURE);
AssetUtils.dispose(shader);
AssetUtils.dispose(batch);
promise.setSuccess(throwable);
}
};
new LwjglApplication(listener, config);
Throwable throwable = promise.awaitUninterruptibly().getNow();
if (throwable != null) throw throwable;
}
}

View File

@ -3,6 +3,7 @@ package com.riiablo.asset.loader;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.FutureListener;
@ -25,6 +26,7 @@ import com.riiablo.RiiabloTest;
import com.riiablo.asset.AssetDesc;
import com.riiablo.asset.AssetManager;
import com.riiablo.asset.AssetUtils;
import com.riiablo.asset.InvalidParams;
import com.riiablo.asset.adapter.GdxFileHandleAdapter;
import com.riiablo.asset.adapter.MpqFileHandleAdapter;
import com.riiablo.asset.param.DcParams;
@ -34,6 +36,8 @@ import com.riiablo.file.Dc;
import com.riiablo.file.Dcc;
import com.riiablo.file.Palette;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.logger.Level;
import com.riiablo.logger.LogManager;
import com.riiablo.mpq_bytebuf.MpqFileHandle;
import com.riiablo.mpq_bytebuf.MpqFileResolver;
import com.riiablo.util.InstallationFinder;
@ -45,6 +49,11 @@ import static com.riiablo.graphics.PaletteIndexedPixmap.INDEXED;
public class DccLoaderTest {
AssetManager assets;
@BeforeAll
public static void beforeAll() {
LogManager.setLevel("com.riiablo.asset.AssetManager", Level.TRACE);
}
@BeforeEach
public void beforeEach() throws DefaultNotFound {
RiiabloTest.clearGdxContext();
@ -66,6 +75,60 @@ public class DccLoaderTest {
AssetUtils.dispose(assets);
}
/**
* Tests that loading DCC with combine frames flag throws IllegalParams exception
*/
@ParameterizedTest
@ValueSource(strings = {
"data\\global\\chars\\ba\\hd\\bahdbhma11hs.dcc",
"data\\global\\chars\\ba\\lg\\balglittnhth.dcc",
"data\\global\\chars\\ba\\hd\\bahdlittnhth.dcc",
"data\\global\\chars\\ba\\tr\\batrlittnhth.dcc",
})
void fail(String dccName) throws Throwable {
AssetDesc<Dcc> asset = AssetDesc.of(dccName, Dcc.class, DcParams.of(0, true));
EventExecutor executor = ImmediateEventExecutor.INSTANCE;
final Promise<Throwable> promise = executor.newPromise();
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration() {{
title = dccName;
forceExit = false;
}};
ApplicationListener listener = new ApplicationAdapter() {
Throwable failure;
Throwable success;
@Override
public void create() {
try {
create0();
} catch (Throwable t) {
t.printStackTrace(System.err);
failure = t;
} finally {
Gdx.app.exit();
}
}
public void create0() throws InterruptedException {
assets.load(asset)
.addListener((FutureListener<Dcc>) future -> success = future.cause());
assets.awaitAll(asset);
}
@Override
public void dispose() {
assets.unload(asset);
if (failure != null) promise.setFailure(failure);
else promise.setSuccess(success);
}
};
new LwjglApplication(listener, config);
Throwable success = promise.awaitUninterruptibly().getNow();
if (success == null) throw promise.cause();
assertEquals(InvalidParams.class, success.getClass());
success.printStackTrace();
}
@ParameterizedTest
@ValueSource(strings = {
"data\\global\\chars\\ba\\hd\\bahdbhma11hs.dcc",
@ -103,7 +166,7 @@ public class DccLoaderTest {
}
public void create0() throws InterruptedException {
Dcc.MISSING_TEXTURE = new Texture(0, 0, INDEXED);
Dc.MISSING_TEXTURE = new Texture(0, 0, INDEXED);
assets.load(asset)
.addListener((FutureListener<Dcc>) future -> dcc = future.getNow());
@ -150,7 +213,7 @@ public class DccLoaderTest {
public void dispose() {
assets.unload(paletteAsset);
assets.unload(asset);
AssetUtils.dispose(Dcc.MISSING_TEXTURE);
AssetUtils.dispose(Dc.MISSING_TEXTURE);
AssetUtils.dispose(shader);
AssetUtils.dispose(batch);
promise.setSuccess(throwable);

View File

@ -83,7 +83,7 @@ public class Dc6DecoderTest {
void create0() {
decoder.decode(dc6, 0);
dc6.uploadTextures(0);
dc6.uploadTextures(0, false);
String paletteName = "data\\global\\palette\\ACT1\\pal.dat";
AssetDesc<Palette> paletteDesc = AssetDesc.of(paletteName, Palette.class, MpqParams.of());

View File

@ -1,6 +1,8 @@
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;
@ -22,13 +24,16 @@ public class Dc6Test extends RiiabloTest {
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 {
@ParameterizedTest
@ValueSource(strings = {
"data\\global\\monsters\\ty\\ra\\tyralitnuhth.dc6",
"data\\global\\ui\\panel\\invchar6.dc6",
"data\\global\\ui\\Loading\\loadingscreen.dc6",
})
void test(String dc6Name) 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 {

View File

@ -191,7 +191,7 @@ public class DccDecoderTest {
void create0() {
decoder.decode(dcc, 0);
dcc.uploadTextures(0);
dcc.uploadTextures(0, false);
String paletteName = "data\\global\\palette\\ACT1\\pal.dat";
AssetDesc<Palette> paletteDesc = AssetDesc.of(paletteName, Palette.class, MpqParams.of());