Added code-base for new MPQ viewer

This commit is contained in:
Collin Smith 2023-08-05 22:52:35 -07:00
parent 4eba03fc1c
commit 7a8f303164
10 changed files with 3568 additions and 0 deletions

View File

@ -0,0 +1,55 @@
package com.riiablo.asset.loader;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import com.badlogic.gdx.files.FileHandle;
import com.riiablo.asset.Adapter;
import com.riiablo.asset.AssetDesc;
import com.riiablo.asset.AssetLoader;
import com.riiablo.asset.AssetManager;
import com.riiablo.file.Cof;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;
public class CofLoader extends AssetLoader<Cof> {
private static final Logger log = LogManager.getLogger(CofLoader.class);
@Override
protected <F extends FileHandle> Future<?> ioAsync0(
EventExecutor executor,
AssetManager assets,
AssetDesc<Cof> asset,
F handle,
Adapter<F> adapter
) {
log.traceEntry("ioAsync0(executor: {}, asset: {}, handle: {}, adapter: {})", executor, asset, handle, adapter);
return adapter.buffer(executor, handle, 0, (int) handle.length());
}
@Override
protected <F extends FileHandle> Cof loadAsync0(
AssetManager assets,
AssetDesc<Cof> asset,
F handle,
Object data
) {
log.traceEntry("loadAsync0(assets: {}, asset: {}, handle: {}, data: {})", assets, asset, handle, data);
assert data instanceof ByteBuf;
ByteBuf buffer = (ByteBuf) data; // borrowed, don't release
try {
return Cof.read(buffer);
} finally {
ReferenceCountUtil.release(handle);
}
}
@Override
protected Cof loadSync0(AssetManager assets, AssetDesc<Cof> asset, Cof cof) {
log.traceEntry("loadSync0(assets: {}, asset: {}, cof: {})", assets, asset, cof);
return super.loadSync0(assets, asset, cof);
}
}

View File

@ -0,0 +1,734 @@
package com.riiablo.file;
import java.util.Arrays;
import org.apache.commons.lang3.Validate;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Affine2;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.utils.BaseDrawable;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pools;
import com.riiablo.Riiablo;
import com.riiablo.codec.Index;
import com.riiablo.codec.util.BBox;
import com.riiablo.graphics.BlendMode;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;
import static com.riiablo.file.Dc.toRealDir;
public class Animation extends BaseDrawable implements Pool.Poolable {
private static final Logger log = LogManager.getLogger(Animation.class);
private static final int DEBUG_MODE = 2; // 0=off, 1=box, 2=layer box
private static final int NUM_LAYERS = Cof.Component.NUM_COMPONENTS;
public static final float FRAMES_PER_SECOND = 25f;
public static final float FRAME_DURATION = 1 / FRAMES_PER_SECOND;
private static final Color SHADOW_TINT = Riiablo.colors.modal75;
private static final Affine2 SHADOW_TRANSFORM = new Affine2();
Cof cof;
int numFrames;
int numDirections;
int direction;
int frame;
int startIndex;
int endIndex;
float frameDuration = FRAME_DURATION;
float elapsedTime;
public enum Mode { ONCE, LOOP, CLAMP }
Mode mode = Mode.LOOP;
boolean reversed;
boolean highlighted;
final Layer layers[] = new Layer[NUM_LAYERS];
final BBox box = new BBox();
private final IntMap<Array<AnimationListener>> EMPTY_MAP = new IntMap<>(0);
private IntMap<Array<AnimationListener>> animationListeners = EMPTY_MAP;
public static Animation newAnimation() {
return new Animation();
}
public static Animation newAnimation(Dc dc) {
return Animation.builder().layer(dc).build();
}
public static Animation newAnimation(Cof cof) {
Animation animation = newAnimation();
animation.setCof(cof);
return animation;
}
@Override
public void reset() {
cof = null;
numFrames = 0;
numDirections = 0;
frame = 0;
direction = 0;
startIndex = 0;
endIndex = 0;
mode = Mode.LOOP;
reversed = false;
frameDuration = FRAME_DURATION;
highlighted = false;
Layer.freeAll(layers);
box.reset();
animationListeners = EMPTY_MAP;
}
public int getNumDirections() {
return numDirections;
}
public int getNumFramesPerDir() {
return numFrames;
}
public int getDirection() {
return direction;
}
public void setDirection(int d) {
if (d != direction) {
Validate.isTrue(0 <= d && d < numDirections, "Invalid direction: " + d);
direction = d;
}
}
public int getFrame() {
return frame;
}
public void setFrame(int f) {
if (f != frame) {
Validate.isTrue(0 <= f && f < numFrames, "Invalid frame: " + f);
frame = f;
elapsedTime = frameDuration * frame;
// if (frame == endIndex - 1) notifyAnimationFinished();
}
}
public float getFrameDuration() {
return frameDuration;
}
public void setFrameDuration(float f) {
frameDuration = f;
elapsedTime = frameDuration * frame;
}
public int getFrameDelta() {
return MathUtils.roundPositive(256f / (frameDuration * FRAMES_PER_SECOND));
}
public void setFrameDelta(int delta) {
setFrameDuration(256f / (delta * FRAMES_PER_SECOND));
}
public int getFrame(float stateTime) {
int frameRange = endIndex - startIndex;
if (frameRange <= 1) return startIndex;
int frameNumber = (int) (stateTime / frameDuration);
switch (mode) {
case ONCE: return startIndex + Math.min(frameRange, frameNumber);
case LOOP: return startIndex + (frameNumber % frameRange);
case CLAMP: return startIndex + Math.min(frameRange - 1, frameNumber);
default: throw new AssertionError("Invalid mode set: " + mode);
}
}
public boolean isFinished() {
return frame == endIndex - 1;
}
public boolean isLooping() {
return mode == Mode.LOOP;
}
public boolean isClamped() {
return mode == Mode.CLAMP;
}
public void setClamp(int startIndex, int endIndex) {
setMode(Mode.CLAMP);
this.startIndex = startIndex;
this.endIndex = endIndex;
}
public Mode getMode() {
return mode;
}
public void setMode(Mode mode) {
assert mode != null;
this.mode = mode;
}
public boolean isReversed() {
return reversed;
}
public void setReversed(boolean b) {
reversed = b;
}
public boolean isHighlighted() {
return highlighted;
}
public void setHighlighted(boolean b) {
if (highlighted != b) {
highlighted = b;
if (b) {
if (cof == null) {
for (int l = 0; l < NUM_LAYERS; l++) {
Layer layer = layers[l];
if (layer == null) break;
if (layer.blendMode == BlendMode.ID) {
layer.setBlendMode(BlendMode.BRIGHTEN, Riiablo.colors.highlight);
}
}
} else {
for (int l = 0; l < cof.numLayers(); l++) {
Cof.Layer cofLayer = cof.layer(l);
Layer layer = layers[cofLayer.component];
if (layer != null && layer.blendMode == BlendMode.ID) { // FIXME: may be unnecessary in production
layer.setBlendMode(BlendMode.BRIGHTEN, Riiablo.colors.highlight);
}
}
}
} else {
if (cof == null) {
for (int l = 0; l < NUM_LAYERS; l++) {
Layer layer = layers[l];
if (layer == null) break;
if (layer.blendMode == BlendMode.BRIGHTEN) {
layer.setBlendMode(BlendMode.ID);
}
}
} else {
for (int l = 0; l < cof.numLayers(); l++) {
Cof.Layer cofLayer = cof.layer(l);
Layer layer = layers[cofLayer.component];
if (layer != null && layer.blendMode == BlendMode.BRIGHTEN) { // FIXME: may be unnecessary in production
layer.setBlendMode(BlendMode.ID);
}
}
}
}
}
}
public Layer getLayerRaw(int i) {
return getLayer(i);
}
public Animation setLayerRaw(int i, Dc dc, boolean updateBox) {
return setLayer(i, dc, updateBox);
}
public Layer getLayer(int component) {
return layers[component];
}
public Animation setLayer(int component, Dc dc) {
return setLayer(component, dc, true);
}
public Animation setLayer(int component, Dc dc, boolean updateBox) {
if (dc == null) {
Layer.free(layers, component);
} else {
if (layers[component] == null) {
layers[component] = Layer.obtain(dc, Layer.DEFAULT_BLENDMODE);
} else {
layers[component].set(dc, Layer.DEFAULT_BLENDMODE);
}
if (updateBox) updateBox();
}
return this;
}
public Layer setLayer(Cof.Layer cofLayer, Dc dc, boolean updateBox) {
setLayer(cofLayer.component, dc, updateBox);
Layer layer = layers[cofLayer.component];
if (layer != null && cofLayer.overrideTransLvl != 0) {
applyTransform(layer, cofLayer.newTransLvl & 0xFF);
}
return layer;
}
private void applyTransform(Layer layer, int transform) {
switch (transform) {
case 0x00:
layer.setBlendMode(layer.blendMode, Riiablo.colors.trans75);
break;
case 0x01:
layer.setBlendMode(layer.blendMode, Riiablo.colors.trans50);
break;
case 0x02:
layer.setBlendMode(layer.blendMode, Riiablo.colors.trans25);
break;
case 0x03:
layer.setBlendMode(BlendMode.LUMINOSITY);
break;
case 0x04:
layer.setBlendMode(BlendMode.LUMINOSITY); // not sure
break;
case 0x06:
layer.setBlendMode(BlendMode.LUMINOSITY); // not sure
break;
default:
log.error("Unknown transform: {}", transform);
}
}
public Cof getCof() {
return cof;
}
public boolean setCof(Cof cof) {
if (this.cof != cof) {
this.cof = cof;
numDirections = cof.numDirections();
numFrames = cof.numFrames();
setFrameDelta(cof.animRate());
if (direction >= numDirections) direction = 0; // FIXME: maybe not necessary if done correctly
frame = 0;
elapsedTime = 0;
startIndex = 0;
endIndex = numFrames;
return true;
}
return false;
}
public BBox getBox() {
return box;
}
public void updateBox() {
if (cof == null) {
box.reset();
for (int l = 0; l < NUM_LAYERS; l++) {
Layer layer = layers[l];
if (layer == null) break;
if (!layer.dc.loaded(direction)) continue;
box.max(layer.dc.box(direction));
}
} else if (frame < numFrames) { // TODO: else assign box to cof.box for dir
int d = toRealDir(direction, cof.numDirections());
int f = frame;
box.reset();
for (int l = 0; l < cof.numLayers(); l++) {
int component = cof.componentAt(d, f, l);
Layer layer = layers[component];
if (layer != null && layer.dc.loaded(d)) {
box.max(layer.dc.box(d));
}
}
}
}
@Override
public float getMinWidth() {
return box.width;
}
@Override
public float getMinHeight() {
return box.height;
}
public void act() {
update();
}
public void act(float delta) {
update(delta);
}
public void update() {
update(Gdx.graphics.getDeltaTime());
}
public void update(float delta) {
elapsedTime += delta;
frame = getFrame(elapsedTime);
if (reversed) frame = endIndex - 1 - frame;
notifyListeners(frame);
if (frame == endIndex - 1) notifyAnimationFinished();
}
public void drawDebug(ShapeRenderer shapes, float x, float y) {
if (DEBUG_MODE == 0) {
return;
} else if (DEBUG_MODE == 1 || cof == null) {
boolean reset = !shapes.isDrawing();
if (reset) {
shapes.begin(ShapeRenderer.ShapeType.Line);
} else {
shapes.set(ShapeRenderer.ShapeType.Line);
}
shapes.setColor(Color.RED);
shapes.line(x, y, x + 50, y);
shapes.setColor(Color.GREEN);
shapes.line(x, y, x, y + 50);
shapes.setColor(Color.BLUE);
shapes.line(x, y, x + 15, y - 20);
shapes.setColor(Color.GREEN);
shapes.rect(x + box.xMin, y - box.yMax, box.width, box.height);
if (reset) shapes.end();
} else if (DEBUG_MODE == 2 && frame < numFrames) {
int d = toRealDir(direction, cof.numDirections());
int f = frame;
for (int l = 0; l < cof.numLayers(); l++) {
int component = cof.componentAt(d, f, l);
Layer layer = layers[component];
if (layer != null) layer.drawDebug(shapes, direction, f, x, y);
}
}
}
public void draw(Batch batch, float x, float y) {
draw(batch, x, y, getMinWidth(), getMinHeight());
}
@Override
public void draw(Batch batch, float x, float y, float width, float height) {
draw((PaletteIndexedBatch) batch, x, y);
}
public void draw(PaletteIndexedBatch batch, float x, float y) {
if (frame >= numFrames) return;
if (cof == null) {
for (Layer layer : layers) {
if (layer == null) continue;
drawLayer(batch, layer, x, y);
}
} else {
int d = toRealDir(direction, cof.numDirections());
int f = frame;
// TODO: Layer blend modes should correspond with the cof trans levels
for (int l = 0, numLayers = cof.numLayers(); l < numLayers; l++) {
int component = cof.componentAt(d, f, l);
Layer layer = layers[component];
if (layer == null) continue;
drawLayer(batch, layer, x, y);
}
}
batch.resetBlendMode();
batch.resetColormap();
}
public void drawLayer(PaletteIndexedBatch batch, Layer layer, float x, float y) {
layer.draw(batch, direction, frame, x, y);
}
public void drawShadow(PaletteIndexedBatch batch, float x, float y) {
drawShadow(batch, x, y, true);
}
public void drawShadow(PaletteIndexedBatch batch, float x, float y, boolean handleBlends) {
if (handleBlends) batch.setBlendMode(BlendMode.SOLID, SHADOW_TINT);
if (cof == null) {
for (Layer layer : layers) {
if (layer == null || !layer.shadow) continue;
drawShadow(batch, layer, x, y);
}
} else if (frame < numFrames) {
int d = toRealDir(direction, cof.numDirections());
int f = frame;
for (int l = 0; l < cof.numLayers(); l++) {
int component = cof.componentAt(d, f, l);
Layer layer = layers[component];
if (layer != null) {
Cof.Layer cofLayer = cof.layerAt(component);
if (cofLayer.shadow == 0x1) {
drawShadow(batch, layer, x, y);
}
}
}
}
if (handleBlends) batch.resetBlendMode();
}
public void drawShadow(PaletteIndexedBatch batch, Layer layer, float x, float y) {
if (frame >= numFrames) {
return;
}
int d = direction;
int f = frame;
Dc dc = layer.dc;
BBox box = dc.box(d, f);
SHADOW_TRANSFORM.idt();
SHADOW_TRANSFORM.preTranslate(box.xMin, -(box.yMax / 2));
SHADOW_TRANSFORM.preShear(-1.0f, 0);
SHADOW_TRANSFORM.preTranslate(x, y);
SHADOW_TRANSFORM.scale(1, 0.5f);
if (f >= layer.numFrames) return; // FIXME: see #113
TextureRegion region = layer.dc.direction(d).frame(f).texture();
if (region.getTexture().getTextureObjectHandle() == 0) return;
batch.draw(region, region.getRegionWidth(), region.getRegionHeight(), SHADOW_TRANSFORM);
}
private void notifyAnimationFinished() {
if (animationListeners == EMPTY_MAP) return;
Array<AnimationListener> listeners = animationListeners.get(-1);
if (listeners == null) return;
for (AnimationListener l : listeners) l.onTrigger(this, -1);
}
private void notifyListeners(int frame) {
if (animationListeners == EMPTY_MAP) return;
Array<AnimationListener> listeners = animationListeners.get(frame);
if (listeners == null) return;
for (AnimationListener l : listeners) l.onTrigger(this, frame);
}
public boolean addAnimationListener(int frame, AnimationListener l) {
Validate.isTrue(l != null, "l cannot be null");
if (animationListeners == EMPTY_MAP) animationListeners = new IntMap<>(1);
Array<AnimationListener> listeners = animationListeners.get(frame);
if (listeners == null) animationListeners.put(frame, listeners = new Array<>(1));
listeners.add(l);
return true;
}
public boolean removeAnimationListener(int frame, AnimationListener l) {
if (l == null || animationListeners == EMPTY_MAP) return false;
Array<AnimationListener> listeners = animationListeners.get(frame);
if (listeners == null) return false;
return listeners.removeValue(l, true);
}
public boolean containsAnimationListener(int frame, AnimationListener l) {
if (l == null || animationListeners == EMPTY_MAP) return false;
Array<AnimationListener> listeners = animationListeners.get(frame);
if (listeners == null) return false;
return listeners.contains(l, true);
}
public interface AnimationListener {
void onTrigger(Animation animation, int frame);
}
public static class Layer implements Pool.Poolable {
private static final Pool<Layer> pool = Pools.get(Layer.class, 1024);
private final Color DEBUG_COLOR = new Color(MathUtils.random(), MathUtils.random(), MathUtils.random(), 1);
static final int DEFAULT_BLENDMODE = BlendMode.ID;
Dc dc;
int numDirections;
int numFrames;
int blendMode;
Color tint;
Index transform;
int transformColor;
boolean shadow;
static Layer obtain(Dc dc, int blendMode) {
return pool.obtain().set(dc, blendMode);
}
static void freeAll(Layer[] layers) {
for (int i = 0; i < layers.length; i++) {
free(layers, i);
}
}
static void free(Layer[] layers, int i) {
Layer layer = layers[i];
if (layer != null) {
pool.free(layer);
layers[i] = null;
}
}
Layer set(Dc dc, int blendMode) {
this.dc = dc;
this.blendMode = blendMode;
tint = Color.WHITE;
numDirections = dc.numDirections();
numFrames = dc.numFrames();
transform = null;
transformColor = 0;
shadow = (blendMode != BlendMode.LUMINOSITY && blendMode != BlendMode.LUMINOSITY_TINT);
return this;
}
@Override
public void reset() {} // Does nothing -- call Layer#set(Dc,int) when obtained
public Dc getDc() {
return dc;
}
public Layer setBlendMode(int blendMode) {
return setBlendMode(blendMode, Color.WHITE);
}
public Layer setBlendMode(int blendMode, Color tint) {
this.blendMode = blendMode;
this.tint = tint;
return this;
}
public Layer setAlpha(float a) {
if (tint == Color.WHITE) tint = tint.cpy();
tint.a = a;
return this;
}
public Layer setTransform(Index colormap, int id) {
transform = colormap;
transformColor = colormap == null ? 0 : id;
return this;
}
public Layer setTransform(byte packedTransform) {
int transform = packedTransform & 0xFF;
if (transform == 0xFF) {
return setTransform(null, 0);
} else {
return setTransform(Riiablo.colormaps.get(transform >>> 5), transform & 0x1F);
}
}
protected void draw(Batch batch, int d, int f, float x, float y) {
if (!dc.loaded(d)) return;
BBox box = dc.box(d, f);
x += box.xMin;
y -= box.yMax;
if (f >= numFrames) return; // FIXME: see #113
TextureRegion region = dc.direction(d).frame(f).texture();
if (region.getTexture().getTextureObjectHandle() == 0) return;
PaletteIndexedBatch b = (PaletteIndexedBatch) batch;
b.setBlendMode(blendMode, tint, true);
b.setColormap(transform, transformColor);
b.draw(region, x, y);
}
protected void drawDebug(ShapeRenderer shapeRenderer, int d, int f, float x, float y) {
if (!dc.loaded(d)) return;
boolean reset = !shapeRenderer.isDrawing();
if (reset) {
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
} else {
shapeRenderer.set(ShapeRenderer.ShapeType.Line);
}
shapeRenderer.setColor(Color.RED);
shapeRenderer.line(x, y, x + 40, y);
shapeRenderer.setColor(Color.GREEN);
shapeRenderer.line(x, y, x, y + 20);
shapeRenderer.setColor(Color.BLUE);
shapeRenderer.line(x, y, x + 20, y - 10);
BBox box = dc.box(d, f);
shapeRenderer.setColor(DEBUG_COLOR);
shapeRenderer.rect(x + box.xMin, y - box.yMax, box.width, box.height);
if (reset) shapeRenderer.end();
}
}
public Builder edit() {
return Builder.obtain(this);
}
public static Builder builder() {
return Builder.obtain(null);
}
public static class Builder implements Pool.Poolable {
private static final Pool<Builder> pool = Pools.get(Builder.class, 32);
Animation animation;
final Layer layers[] = new Layer[NUM_LAYERS];
int size = 0;
public static Builder obtain(Animation animation) {
Builder builder = pool.obtain();
builder.animation = animation;
return builder;
}
@Override
public void reset() {
Arrays.fill(layers, 0, size, null);
size = 0;
}
public Builder layer(Dc dc) {
return layer(Layer.obtain(dc, Layer.DEFAULT_BLENDMODE));
}
public Builder layer(Dc dc, int blendMode) {
return layer(Layer.obtain(dc, blendMode));
}
public Builder layer(Dc dc, int blendMode, byte packedTransform) {
Layer layer = Layer.obtain(dc, blendMode);
layer.setTransform(packedTransform);
return layer(layer);
}
public Builder layer(Layer layer) {
layers[size++] = layer;
return this;
}
public Animation build() {
Layer first = layers[0];
if (animation == null) {
animation = Animation.newAnimation();
} else {
animation.reset();
}
animation.numDirections = first.numDirections;
animation.numFrames = first.numFrames;
animation.startIndex = 0;
animation.endIndex = animation.numFrames;
animation.frame = animation.startIndex;
animation.elapsedTime = 0;
System.arraycopy(layers, 0, animation.layers, 0, size);
animation.updateBox();
pool.free(this);
return animation;
}
}
}

View File

@ -0,0 +1,255 @@
package com.riiablo.file;
import io.netty.buffer.ByteBuf;
import org.apache.commons.lang3.ArrayUtils;
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 final class Cof {
private static final Logger log = LogManager.getLogger(Cof.class);
public static final class Component {
public static final byte HD = 0x0; // head
public static final byte TR = 0x1; // torso
public static final byte LG = 0x2; // legs
public static final byte RA = 0x3; // right arm
public static final byte LA = 0x4; // left arm
public static final byte RH = 0x5; // right hand
public static final byte LH = 0x6; // left hand
public static final byte SH = 0x7; // shield
public static final byte S1 = 0x8; // special 1
public static final byte S2 = 0x9; // special 2
public static final byte S3 = 0xA; // special 3
public static final byte S4 = 0xB; // special 4
public static final byte S5 = 0xC; // special 5
public static final byte S6 = 0xD; // special 6
public static final byte S7 = 0xE; // special 7
public static final byte S8 = 0xF; // special 8
public static final int NUM_COMPONENTS = 16;
private static final String[] NAME = {
"HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"
};
public static String toString(byte value) {
return NAME[value];
}
public static String[] values() {
return NAME;
}
}
public static Cof read(ByteBuf buffer) {
return read(ByteInput.wrap(buffer));
}
public static Cof read(ByteInput in) {
final int size = in.bytesRemaining();
short numLayers = in.read8u();
short numFrames = in.read8u(); // frames before dirs
short numDirections = in.read8u();
short version = in.read8u();
byte[] unk = in.readBytes(4);
int xMin = in.read32();
int xMax = in.read32();
int yMin = in.read32();
int yMax = in.read32();
int animRate = in.readSafe32u();
BBox box = new BBox(xMin, yMin, xMax, yMax);
log.trace("version: {}", version);
log.trace("numLayers: {}", numLayers);
log.trace("numDirections: {}", numDirections);
log.trace("numFrames: {}", numFrames);
log.trace("unk: {}", DebugUtils.toByteArray(unk));
log.trace("box: {}", box);
log.trace("animRate: {}", animRate);
Layer[] layers = new Layer[numLayers];
for (int l = 0; l < numLayers; l++) {
try {
MDC.put("layer", l);
layers[l] = new Layer(in);
} finally {
MDC.remove("layer");
}
}
final int keyframesSize;
if (size == 42 && numLayers == 1 && numDirections == 1 && numFrames == 1) {
// not sure if this is a special case or not, min kf #?
keyframesSize = 4;
} else {
keyframesSize = numFrames;
}
log.trace("keyframesSize: {}", keyframesSize);
byte[] keyframes = in.readBytes(keyframesSize);
log.trace("keyframes: {}", keyframes);
byte[] layerOrder = in.readBytes(numDirections * numFrames * numLayers);
if (log.traceEnabled()) {
StringBuilder builder = new StringBuilder(16384).append('\n');
for (int d = 0, i = 0; d < numDirections; d++) {
builder.append(String.format("%2d", d)).append(':').append(' ');
for (int f = 0; f < numFrames; f++) {
builder.append('[');
for (int l = 0; l < numLayers; l++) {
byte b = layerOrder[i++];
builder.append(Component.toString(b)).append(' ');
}
builder.setLength(builder.length() - 1);
builder.append(']').append(',').append(' ');
}
builder.setLength(builder.length() - 2);
builder.append('\n');
}
builder.setLength(builder.length() - 1);
log.trace("layerOrder: {}", builder.toString());
}
assert in.bytesRemaining() == 0;
return new Cof(version, numDirections, numFrames, numLayers, unk, box, animRate, layers, keyframes, layerOrder);
}
final short numDirections; // ubyte
final short numFrames; // ubyte
final short numLayers; // ubyte
final short version; // ubyte
final byte[] unk;
final BBox box;
final int animRate; // uint
final Layer[] layers;
final byte[] keyframes;
final byte[] layerOrder;
final byte[] components; // derived
Cof(
short version,
short numDirections,
short numFrames,
short numLayers,
byte[] unk,
BBox box,
int animRate,
Layer[] layers,
byte[] keyframes,
byte[] layerOrder
) {
this.version = version;
this.numLayers = numLayers;
this.numDirections = numDirections;
this.numFrames = numFrames;
this.unk = unk;
this.box = box;
this.animRate = animRate;
this.layers = layers;
this.keyframes = keyframes;
this.layerOrder = layerOrder;
components = new byte[16];
for (byte i = 0; i < layers.length; i++) {
components[layers[i].component] = i;
}
}
public int numDirections() {
return numDirections;
}
public int numFrames() {
return numFrames;
}
public int numLayers() {
return numLayers;
}
public int animRate() {
return animRate;
}
public Layer layer(int l) {
return layers[l];
}
public Layer layerAt(int component) {
return layers[components[component]];
}
public int findKeyframe(Keyframe keyframe) {
return ArrayUtils.indexOf(keyframes, keyframe.asInt());
}
public byte componentAt(int d, int f, int l) {
final int dfl = d * numFrames * numLayers;
final int df = f * numLayers;
return layerOrder[dfl + df + l];
}
public static final class Layer {
public byte component; // ubyte
public byte shadow; // ubyte
public byte selectable; // ubyte
public byte overrideTransLvl; // ubyte
public byte newTransLvl; // ubyte
public String weaponClass;
Layer(ByteInput in) {
component = in.readSafe8u();
shadow = in.readSafe8u();
selectable = in.readSafe8u();
overrideTransLvl = in.readSafe8u();
newTransLvl = in.readSafe8u();
weaponClass = in.readString(4);
log.trace("component: {}", component);
log.trace("shadow: {}", shadow);
log.trace("selectable: {}", selectable);
log.trace("overrideTransLvl: {}", overrideTransLvl);
log.trace("newTransLvl: {}", newTransLvl);
log.trace("weaponClass: {}", weaponClass);
}
}
public enum Keyframe {
NONE((byte) 0),
ATTACK((byte) 1),
MISSILE((byte) 2),
SOUND((byte) 3),
SKILL((byte) 4),
;
public static Keyframe fromInt(byte i) {
switch (i) {
case 0: return NONE;
case 1: return ATTACK;
case 2: return MISSILE;
case 3: return SOUND;
case 4: return SKILL;
default: throw new IllegalArgumentException(i + " does not map to any known keyframe constant!");
}
}
final byte value;
Keyframe(byte value) {
this.value = value;
}
public byte asInt() {
return value;
}
}
}

View File

@ -1,3 +1,4 @@
dependencies { implementation project(':tools:backends:backend-lwjgl3') }
dependencies { implementation ("com.kotcrab.vis:vis-ui") { version { require '1.5.0' } } }
description = 'View and debug MPQ archive contents.'
application.mainClass = 'com.riiablo.mpq.MPQViewer'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
package com.riiablo.tool.mpqviewer.widget;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.kotcrab.vis.ui.widget.VisImageButton;
public class BorderedVisImageButton extends VisImageButton {
protected ClickListener clickListener;
protected boolean hasFocus;
protected Drawable focusBorder;
public BorderedVisImageButton(VisImageButtonStyle style, Drawable focusBorder, String tooltip) {
super(style.imageUp, tooltip);
this.focusBorder = focusBorder;
setStyle(style);
addListener(clickListener = new ClickListener());
}
@Override
public void focusGained() {
super.focusGained();
hasFocus = true;
}
@Override
public void focusLost() {
super.focusLost();
hasFocus = false;
}
@Override
public void draw(Batch batch, float parentAlpha) {
super.draw(batch, parentAlpha);
if (clickListener.isOver()) {
focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight());
}
}
}

View File

@ -0,0 +1,74 @@
package com.riiablo.tool.mpqviewer.widget;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.scenes.scene2d.utils.BaseDrawable;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.kotcrab.vis.ui.VisUI;
import com.kotcrab.vis.ui.widget.VisTextField;
public class BorderedVisTextField extends VisTextField {
static final VisTextFieldStyle style;
static {
style = VisUI.getSkin().get("light", VisTextFieldStyle.class);
final Drawable backgroundParent = style.background;
style.background = new BaseDrawable(backgroundParent) {
{
setLeftWidth(4);
setRightWidth(4);
}
@Override
public void draw(Batch batch, float x, float y, float width, float height) {
backgroundParent.draw(batch, x, y, width, height);
}
};
final Drawable backgroundOverParent = style.backgroundOver;
style.backgroundOver = new BaseDrawable(backgroundOverParent) {
{
setLeftWidth(4);
setRightWidth(4);
}
@Override
public void draw(Batch batch, float x, float y, float width, float height) {
backgroundOverParent.draw(batch, x, y, width, height);
}
};
}
protected ClickListener clickListener;
protected boolean hasFocus;
public BorderedVisTextField() {
super();
setStyle(style);
}
@Override
protected void initialize() {
super.initialize();
addListener(clickListener = new ClickListener());
}
@Override
public void focusGained() {
super.focusGained();
hasFocus = true;
}
@Override
public void focusLost() {
super.focusLost();
hasFocus = false;
}
@Override
public void draw(Batch batch, float parentAlpha) {
super.draw(batch, parentAlpha);
final VisTextFieldStyle style = getStyle();
if (clickListener.isOver() || hasFocus) {
style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight());
}
}
}

View File

@ -0,0 +1,77 @@
package com.riiablo.tool.mpqviewer.widget;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntMap;
import static com.riiablo.util.ImplUtils.unsupported;
public class ButtonGroup<T extends Button> extends com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup<T> {
final IntMap<Button> ids = new IntMap<>();
final ClickListener clickListener = new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
Button button = (Button) event.getListenerActor();
if (button.isDisabled()) return;
notifyButtonSwitched(getButtons().indexOf((T) button, true));
}
};
final Array<ButtonGroupListener> listeners = new Array<>();
public ButtonGroup() {
super();
}
public void addListener(ButtonGroupListener listener) {
listeners.add(listener);
}
public int addId(T button) {
int index = getButtons().size;
ids.put(index, button);
super.add(button);
button.addListener(clickListener);
return index;
}
@Override
public final void add(T button) {
unsupported("Not supported.");
}
public void setChecked(int index) {
ids.get(index).setChecked(true);
}
public void setDisabled(int index, boolean b) {
ids.get(index).setDisabled(b);
}
@Override
public final void clear() {
unsupported("Not supported.");
}
@Override
public final void remove(T... buttons) {
unsupported("Not supported.");
}
@Override
public final void remove(T button) {
unsupported("Not supported.");
}
private void notifyButtonSwitched(int toIndex) {
for (ButtonGroupListener l : listeners) {
l.switchedButton(toIndex);
}
}
public interface ButtonGroupListener {
void switchedButton(int toIndex);
}
}

View File

@ -0,0 +1,62 @@
package com.riiablo.tool.mpqviewer.widget;
import com.kotcrab.vis.ui.widget.VisTable;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.Value;
public class CollapsibleVisTable extends VisTable {
private boolean collapsed = false;
private float currentWidth = -1;
public CollapsibleVisTable() {
this(false);
}
public CollapsibleVisTable(boolean setVisDefaults) {
super(setVisDefaults);
}
public boolean collapsed() {
return collapsed;
}
public void setCollapsed(boolean collapsed) {
if (this.collapsed == collapsed) {
return;
}
this.collapsed = collapsed;
if (collapsed) {
setTouchable(Touchable.disabled);
currentWidth = 0;
} else {
setTouchable(Touchable.childrenOnly);
currentWidth = super.getPrefWidth();
}
invalidateHierarchy();
}
public boolean isCollapsed() {
return collapsed;
}
@Override
public void draw(Batch batch, float a) {
if (currentWidth > 0) super.draw(batch, a);
}
@Override
public float getMinWidth() {
return collapsed ? 0 : super.getMinWidth();
}
@Override
public float getPrefWidth() {
if (currentWidth < 0) currentWidth = super.getPrefWidth();
return currentWidth;
}
}

View File

@ -0,0 +1,100 @@
package com.riiablo.tool.mpqviewer.widget;
import com.kotcrab.vis.ui.VisUI;
import com.kotcrab.vis.ui.widget.VisTable;
import com.kotcrab.vis.ui.widget.VisTextButton;
import java.util.concurrent.CopyOnWriteArrayList;
import com.badlogic.gdx.scenes.scene2d.ui.Container;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.utils.IntMap;
import com.riiablo.tool.mpqviewer.widget.ButtonGroup.ButtonGroupListener;
public class TabbedPane extends VisTable {
VisTable tabs;
Container<VisTable> contentPanel;
ButtonGroup<VisTextButton> buttons = new ButtonGroup<>();
IntMap<VisTable> map = new IntMap<>();
CopyOnWriteArrayList<TabListener> listeners = new CopyOnWriteArrayList<>();
final ButtonGroupListener groupListener = toIndex -> {
contentPanel.setActor(map.get(toIndex));
notifyTabSwitched(toIndex);
};
public TabbedPane() {
setBackground(VisUI.getSkin().getDrawable("default-pane"));
buttons.addListener(groupListener);
add(tabs = new VisTable()).growX().row();
add(new Image(VisUI.getSkin().getDrawable("list-selection"))).minHeight(1).growX().row();
add(contentPanel = new Container<>())
.pad(4)
.row();
}
public int addTab(String text, VisTable content) {
VisTextButton button = new VisTextButton(text) {{
setName(getText().toString());
setStyle(new TextButtonStyle(getStyle()) {{
checked = down;
}});
setProgrammaticChangeEvents(false);
setFocusBorderEnabled(false);
}};
int index = buttons.addId(button);
tabs.add(button).growX();
map.put(index, content);
if (buttons.getCheckedIndex() == -1) {
contentPanel.setActor(content);
notifyTabSwitched(index);
}
return index;
}
public boolean switchTo(int tabIndex) {
if (getTabIndex() == tabIndex) return false;
buttons.setChecked(tabIndex); // Doesn't actually fire button
groupListener.switchedButton(tabIndex);
return true;
}
public void update() {
contentPanel.setActor(map.get(getTabIndex()));
}
public String getTab() {
return buttons.getChecked().getName();
}
public int getTabIndex() {
return buttons.getCheckedIndex();
}
public void setDisabled(int tabIndex, boolean b) {
buttons.setDisabled(tabIndex, b);
if (b && getTabIndex() == tabIndex) {
switchTo(tabIndex + 1 >= buttons.getButtons().size ? 0 : tabIndex + 1);
}
}
public void addListener(TabListener l) {
listeners.add(l);
}
public boolean removeListener(TabListener l) {
return listeners.remove(l);
}
private void notifyTabSwitched(int tabIndex) {
for (TabListener l : listeners) {
l.switchedTab(tabIndex);
}
}
public interface TabListener {
void switchedTab(int tabIndex);
}
}