diff --git a/core/src/main/java/com/riiablo/asset/loader/CofLoader.java b/core/src/main/java/com/riiablo/asset/loader/CofLoader.java new file mode 100644 index 00000000..7a9afd59 --- /dev/null +++ b/core/src/main/java/com/riiablo/asset/loader/CofLoader.java @@ -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 { + private static final Logger log = LogManager.getLogger(CofLoader.class); + + @Override + protected Future ioAsync0( + EventExecutor executor, + AssetManager assets, + AssetDesc asset, + F handle, + Adapter adapter + ) { + log.traceEntry("ioAsync0(executor: {}, asset: {}, handle: {}, adapter: {})", executor, asset, handle, adapter); + return adapter.buffer(executor, handle, 0, (int) handle.length()); + } + + @Override + protected Cof loadAsync0( + AssetManager assets, + AssetDesc 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 asset, Cof cof) { + log.traceEntry("loadSync0(assets: {}, asset: {}, cof: {})", assets, asset, cof); + return super.loadSync0(assets, asset, cof); + } +} diff --git a/core/src/main/java/com/riiablo/file/Animation.java b/core/src/main/java/com/riiablo/file/Animation.java new file mode 100644 index 00000000..9230c1b2 --- /dev/null +++ b/core/src/main/java/com/riiablo/file/Animation.java @@ -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> EMPTY_MAP = new IntMap<>(0); + private IntMap> 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 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 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 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 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 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 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 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; + } + } +} diff --git a/core/src/main/java/com/riiablo/file/Cof.java b/core/src/main/java/com/riiablo/file/Cof.java new file mode 100644 index 00000000..7d2a9590 --- /dev/null +++ b/core/src/main/java/com/riiablo/file/Cof.java @@ -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; + } + } +} diff --git a/tools/mpq-viewer/build.gradle b/tools/mpq-viewer/build.gradle index 863e8ffd..e377cb1b 100644 --- a/tools/mpq-viewer/build.gradle +++ b/tools/mpq-viewer/build.gradle @@ -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' diff --git a/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/MpqViewer.java b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/MpqViewer.java new file mode 100644 index 00000000..03735705 --- /dev/null +++ b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/MpqViewer.java @@ -0,0 +1,2171 @@ +package com.riiablo.tool.mpqviewer; + +import com.kotcrab.vis.ui.VisUI; +import com.kotcrab.vis.ui.util.dialog.Dialogs; +import com.kotcrab.vis.ui.widget.Menu; +import com.kotcrab.vis.ui.widget.MenuBar; +import com.kotcrab.vis.ui.widget.MenuItem; +import com.kotcrab.vis.ui.widget.PopupMenu; +import com.kotcrab.vis.ui.widget.VisCheckBox; +import com.kotcrab.vis.ui.widget.VisImageButton; +import com.kotcrab.vis.ui.widget.VisLabel; +import com.kotcrab.vis.ui.widget.VisList; +import com.kotcrab.vis.ui.widget.VisScrollPane; +import com.kotcrab.vis.ui.widget.VisSelectBox; +import com.kotcrab.vis.ui.widget.VisSlider; +import com.kotcrab.vis.ui.widget.VisSplitPane; +import com.kotcrab.vis.ui.widget.VisTable; +import com.kotcrab.vis.ui.widget.VisTextButton; +import com.kotcrab.vis.ui.widget.VisTextField; +import com.kotcrab.vis.ui.widget.VisTree; +import com.kotcrab.vis.ui.widget.color.ColorPicker; +import com.kotcrab.vis.ui.widget.color.ColorPickerAdapter; +import com.kotcrab.vis.ui.widget.file.FileChooser; +import com.kotcrab.vis.ui.widget.file.FileChooserAdapter; +import com.kotcrab.vis.ui.widget.file.internal.PreferencesIO; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.Locale; +import java.util.Objects; +import java.util.SortedMap; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.collections4.Trie; +import org.apache.commons.collections4.trie.PatriciaTrie; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.Input.Keys; +import com.badlogic.gdx.Preferences; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.EventListener; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.InputListener; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Button; +import com.badlogic.gdx.scenes.scene2d.ui.Cell; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Stack; +import com.badlogic.gdx.scenes.scene2d.ui.Tree.Node; +import com.badlogic.gdx.scenes.scene2d.utils.BaseDrawable; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; +import com.badlogic.gdx.scenes.scene2d.utils.Drawable; +import com.badlogic.gdx.scenes.scene2d.utils.Selection; +import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; +import com.badlogic.gdx.scenes.scene2d.utils.UIUtils; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.I18NBundle; +import com.badlogic.gdx.utils.ObjectIntMap; +import com.badlogic.gdx.utils.ObjectMap; +import com.badlogic.gdx.utils.viewport.ScreenViewport; + +import com.riiablo.Colors; +import com.riiablo.Riiablo; +import com.riiablo.asset.AssetDesc; +import com.riiablo.asset.AssetManager; +import com.riiablo.asset.AssetUtils; +import com.riiablo.asset.adapter.MpqFileHandleAdapter; +import com.riiablo.asset.loader.CofLoader; +import com.riiablo.asset.loader.Dc6Loader; +import com.riiablo.asset.loader.DccLoader; +import com.riiablo.asset.param.DcParams; +import com.riiablo.asset.param.MpqParams; +import com.riiablo.file.Animation; +import com.riiablo.file.Cof; +import com.riiablo.file.CofInfo; +import com.riiablo.file.Dc; +import com.riiablo.file.Dc6; +import com.riiablo.file.Dc6Info; +import com.riiablo.file.Dcc; +import com.riiablo.file.DccInfo; +import com.riiablo.file.Palette; +import com.riiablo.graphics.BlendMode; +import com.riiablo.graphics.PaletteIndexedBatch; +import com.riiablo.logger.Level; +import com.riiablo.logger.LogManager; +import com.riiablo.logger.Logger; +import com.riiablo.mpq.widget.DirectionActor; +import com.riiablo.mpq_bytebuf.MpqFileHandle; +import com.riiablo.mpq_bytebuf.MpqFileResolver; +import com.riiablo.tool.Lwjgl3Tool; +import com.riiablo.tool.Tool; +import com.riiablo.tool.mpqviewer.widget.BorderedVisImageButton; +import com.riiablo.tool.mpqviewer.widget.BorderedVisTextField; +import com.riiablo.tool.mpqviewer.widget.CollapsibleVisTable; +import com.riiablo.tool.mpqviewer.widget.TabbedPane; + +import static com.badlogic.gdx.utils.Align.bottomRight; +import static com.badlogic.gdx.utils.Align.center; +import static com.badlogic.gdx.utils.Align.top; +import static com.badlogic.gdx.utils.Align.topLeft; +import static com.kotcrab.vis.ui.widget.file.FileChooser.Mode.OPEN; +import static com.kotcrab.vis.ui.widget.file.FileChooser.SelectionMode.DIRECTORIES; + +public class MpqViewer extends Tool { + private static final Logger log = LogManager.getLogger(MpqViewer.class); + + public static void main(String[] args) throws Exception { + Lwjgl3Tool.create(MpqViewer.class, "mpq-viewer", args) + .size(1280, 800, true) // arbitrary, comfortable widget layout + .config((Lwjgl3Tool.Lwjgl3ToolConfigurator) config -> { + config.setWindowSizeLimits(640, 480, -1, -1); + config.useVsync(false); + config.setForegroundFPS(300); + }) + .start(); + } + + public static final ClickListener SCROLL_ON_HOVER = new ClickListener() { + @Override + public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) { + event.getStage().setScrollFocus(event.getTarget()); + } + + @Override + public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) { + event.getStage().setScrollFocus(null); + } + }; + + @Override + protected void createCliOptions(Options options) { + super.createCliOptions(options); + options.addOption(Option + .builder("f") + .longOpt("file") + .desc("initial file to open") + .hasArg() + .argName("path") + .build()); + options.addOption(Option + .builder("d") + .longOpt("debug") + .desc("enabled debug mode") + .build()); + } + + @Override + protected void handleCliOptions(String cmd, Options options, CommandLine cli) throws Exception { + super.handleCliOptions(cmd, options, cli); + initialFile = cli.getOptionValue("file"); + debugMode = cli.hasOption("debug"); + } + + void title(String fileName) { + final String title; + if (fileName == null) { + title = i18n("mpq-viewer"); + } else { + title = i18n("mpq-viewer-with-file", fileName); + } + + log.debug("title -> {}", title); + Gdx.graphics.setTitle(title); + } + + public String i18n(String key, Object... args) { + return bundle.format(key, args); + } + + public static Skin getSkin() { + return instance.mpqViewerAssets; + } + + public static MpqViewer instance; + + static void click(Actor actor) { + InputEvent event = new InputEvent(); + event.setListenerActor(actor); + for (EventListener l : actor.getListeners()) { + if (l instanceof ClickListener) ((ClickListener) l).clicked(event, 0, 0); + } + } + + Skin mpqViewerAssets; + MpqFileResolver mpqs; + AssetManager assets; + + PaletteIndexedBatch batch; + ShaderProgram shader; + ShapeRenderer shapes; + + String initialFile; + boolean debugMode; + + Preferences prefs; + I18NBundle bundle; + + Stage stage; + VisTable root; + + FileChooser fileChooser; + ColorPicker colorPicker; + + MenuBar menu; + Menu fileMenu; + MenuItem file_open; + MenuItem file_exit; + Menu optionsMenu; + MenuItem options_checkExisting; + MenuItem options_useExternalList; + + VisTable content; + VisSplitPane verticalSplit; + VisSplitPane horizontalSplit; + + VisTextField addressBar; + PopupMenu addressBarMenu; + MenuItem address_copy; + MenuItem address_copyFixed; + MenuItem address_paste; + ClickListener address_paste_clickListener; + + VisTree fileTree; + VisScrollPane fileTreeScroller; + VisTextField fileTreeFilter; + Trie fileTreeNodes; + Trie fileTreeCofNodes; + + Renderer renderer; + VisScrollPane rendererScroller; + Cell rendererStack; + FullscreenListener fullscreenListener; + PopupMenu rendererMenu; + MenuItem renderer_changeBackground; + + VisTable controlPanel; + Array controlPanels; + + CollapsibleVisTable animationControls; + TabbedPane animationControlsTabs; + int ANIMATION_TAB = -1; + int PAGE_TAB = -1; + + // Animation tab controls + Button btnPlayPause; + Button btnFirstFrame; + Button btnLastFrame; + Button btnPrevFrame; + Button btnNextFrame; + DirectionActor daDirection; + VisLabel lbDirection; + VisSlider slDirection; + VisLabel lbFrameIndex; + VisSlider slFrameIndex; + VisLabel lbFrameDuration; + VisSlider slFrameDuration; + VisCheckBox cbDebugMode; + VisSelectBox sbBlendMode; + + // Page tab controls + VisLabel lbPage; + VisSlider slPage; + Button btnFirstPage; + Button btnLastPage; + Button btnPrevPage; + Button btnNextPage; + VisLabel lbDirectionPage; + VisSlider slDirectionPage; + VisSelectBox sbBlendModePage; + + CollapsibleVisTable paletteControls; + Trie palettes; + VisList paletteList; + VisScrollPane paletteScroller; + + CollapsibleVisTable dccControls; + DccInfo dccInfo; + + CollapsibleVisTable dc6Controls; + Dc6Info dc6Info; + + CollapsibleVisTable cofControls; + CofInfo cofInfo; + EnumMap lbKeyframes; + VisList components; + VisScrollPane componentScroller; + VisList wclasses; + VisScrollPane wclassScroller; + ObjectMap> compClasses; + String selectedWClass[]; + static final String[] DC_EXTS = new String[] { "DCC", "DC6" }; + static final ObjectIntMap COMP_TO_ID = new ObjectIntMap<>(); + static { + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.HD), Cof.Component.HD); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.TR), Cof.Component.TR); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.LG), Cof.Component.LG); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.RA), Cof.Component.RA); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.LA), Cof.Component.LA); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.RH), Cof.Component.RH); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.LH), Cof.Component.LH); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.SH), Cof.Component.SH); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S1), Cof.Component.S1); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S2), Cof.Component.S2); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S3), Cof.Component.S3); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S4), Cof.Component.S4); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S5), Cof.Component.S5); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S6), Cof.Component.S6); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S7), Cof.Component.S7); + COMP_TO_ID.put(Cof.Component.toString(Cof.Component.S8), Cof.Component.S8); + } + + @Override + public void create() { + instance = this; + Gdx.app.setLogLevel(com.badlogic.gdx.utils.Logger.DEBUG); + LogManager.setLevel(MpqViewer.class.getCanonicalName(), Level.ALL); + + LogManager.setLevel("com.riiablo.file", Level.TRACE); + // LogManager.setLevel("com.riiablo.asset.AssetManager", Level.TRACE); + + prefs = Gdx.app.getPreferences(MpqViewer.class.getCanonicalName()); + bundle = I18NBundle.createBundle(Gdx.files.internal("lang/MpqViewer")); + title(null); + + log.debug("loading VisUI..."); + VisUI.load(Gdx.files.internal("skin/x1/uiskin.json")); + PreferencesIO.setDefaultPrefsName(MpqViewer.class.getCanonicalName()); + FileChooser.setSaveLastDirectory(true); + // TODO: pack mpq-viewer assets with VisUI skin + final TextureAtlas mpqViewerAtlas = new TextureAtlas(Gdx.files.internal("skin/mpq-viewer/mpq-viewer.atlas")); + mpqViewerAssets = new Skin(mpqViewerAtlas); + + log.debug("creating menu bar..."); + menu = new MenuBar() {{ + addMenu(fileMenu = new Menu(i18n("menu-file")) {{ + addItem(file_open = new MenuItem(i18n("menu-open")) {{ + setShortcut(Keys.CONTROL_LEFT, Keys.O); + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + if (fileChooser != null) return; + openMpqs(); + } + }); + }}); + addItem(file_exit = new MenuItem(i18n("menu-exit")) {{ + setShortcut(Keys.ALT_LEFT, Keys.F4); + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + Gdx.app.exit(); // calls #dispose() + } + }); + }}); + }}); + addMenu(optionsMenu = new Menu(i18n("menu-options")) {{ + addItem(options_checkExisting = new MenuItem( + i18n("menu-check-files"), + VisUI.getSkin().getDrawable("check-on") + ) {{ + setChecked(prefs.getBoolean("menu-check-files", true)); + getImageCell().size(getImage().getPrefWidth(), getImage().getPrefHeight()); + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + final boolean isChecked = isChecked(); + prefs.putBoolean("menu-check-files", isChecked).flush(); + getImage().setDrawable(VisUI.getSkin(), isChecked ? "check-on" : "check-off"); + reloadMpqs(); + } + }); + }}); + /* + // TODO: add support for custom listfile + addSeparator(); + addItem(options_useExternalList = new MenuItem( + i18n("menu-custom-listfile"), + VisUI.getSkin().getDrawable("check-off") + ) {{ + setChecked(prefs.getBoolean("menu-custom-listfile", false)); + getImageCell().size(getImage().getPrefWidth(), getImage().getPrefHeight()); + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + final boolean isChecked = isChecked(); + getImage().setDrawable(VisUI.getSkin(), isChecked ? "check-on" : "check-off"); + reloadMpqs(); + } + }); + }}); + */ + }}); + }}; + + content = new VisTable() {{ + add(verticalSplit = new VisSplitPane(null, null, false) {{ + setSplitAmount(0.15f); + setMinSplitAmount(0.00f); + setMaxSplitAmount(1.00f); + setBackground(VisUI.getSkin().getDrawable("grey")); + setFirstWidget(new VisTable() {{ + pad(4); + left(); + add(fileTreeFilter = new BorderedVisTextField() {{ + setMessageText(i18n("filter-hint")); + setFocusTraversal(false); + addListener(new InputListener() { + @Override + public boolean keyDown(InputEvent event, int keycode) { + if (mpqs != null && keycode == Keys.TAB) { + String text = getText().toUpperCase(Locale.ROOT); + if (text.endsWith("\\")) { + return true; + } + + String key; + Node selectedNode = null; + SortedMap prefixMap = fileTreeNodes.prefixMap(text); + if (prefixMap.isEmpty()) { + text = text.trim(); + if (text.length() != 7) { + return true; + } else { + selectedNode = fileTreeCofNodes.get(text); + if (selectedNode == null) return true; + key = text; + log.debug("Found {} at {}", text, selectedNode.getValue()); + } + } else { + key = prefixMap.firstKey(); + } + + setText(key); + setCursorAtTextEnd(); + + if (selectedNode == null) selectedNode = fileTreeNodes.get(key); + if (selectedNode != null) { + fileTree.collapseAll(); + selectedNode.expandTo(); + + Array children = selectedNode.getChildren(); + if (children.size > 0) { + selectedNode.setExpanded(true); + } else { + fileTree.getSelection().set(selectedNode); + } + + fileTree.layout(); + Actor actor = selectedNode.getActor(); + fileTreeScroller.scrollTo(actor.getX(), actor.getY(), actor.getWidth(), actor.getHeight(), false, false); + } + } + + return true; + } + }); + }}).growX().row(); + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + add(fileTreeScroller = new VisScrollPane(fileTree = new VisTree() {{ + TreeStyle style = new TreeStyle(getStyle()); + style.plus = mpqViewerAssets.getDrawable("chevron-right"); + style.minus = mpqViewerAssets.getDrawable("chevron-down"); + setStyle(style); + }}) { + { + setStyle(new ScrollPaneStyle(getStyle()) {{ + vScroll = null; + vScrollKnob = mpqViewerAssets.getDrawable("vscroll"); + }}); + // setForceScroll(false, true); + setFadeScrollBars(false); + setScrollbarsOnTop(true); + addListener(SCROLL_ON_HOVER); + } + + @Override + protected void drawScrollBars(Batch batch, float r, float g, float b, float a) { + super.drawScrollBars(batch, r, g, b, a * 0.5f); + } + }).grow(); + }}).space(4).grow(); + }}); + setSecondWidget(horizontalSplit = new VisSplitPane(null, null, true) {{ + setSplitAmount(0.60f); + setMinSplitAmount(0.50f); + setMaxSplitAmount(1.00f); + setFirstWidget(new VisTable() {{ + pad(4); + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + rendererMenu = new PopupMenu() {{ + addItem(renderer_changeBackground = new MenuItem(i18n("renderer-change-background")) {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + if (colorPicker != null) return; + ColorPicker cp = colorPicker = new ColorPicker( + i18n("renderer-change-background-title"), + new ColorPickerAdapter() { + @Override + public void finished(Color newColor) { + renderer.setBackground(newColor); + dispose(); + } + + @Override + public void canceled(Color oldColor) { + dispose(); + } + + void dispose() { + colorPicker = null; + } + }); + cp.setColor(renderer.getBackground()); + stage.addActor(cp.fadeIn()); + } + }); + }}); + }}; + rendererScroller = new VisScrollPane(renderer = new Renderer()) { + { + // copy "list" style into "renderer scroller" style + setStyle(new ScrollPaneStyle(VisUI.getSkin().get("list", ScrollPaneStyle.class)) {{ + hScroll = null; + hScrollKnob = mpqViewerAssets.getDrawable("hscroll-light"); + vScroll = null; + vScrollKnob = mpqViewerAssets.getDrawable("vscroll-light"); + }}); + // setupFadeScrollBars(0, 0); + setFadeScrollBars(false); + setSmoothScrolling(false); + setFlingTime(0); + setOverscroll(false, false); + addListener(fullscreenListener = new FullscreenListener()); + addListener(new ClickListener(Input.Buttons.RIGHT) { + @Override + public void clicked(InputEvent event, float x, float y) { + rendererMenu.showMenu(stage, event.getStageX(), event.getStageY()); + } + }); + } + + @Override + public void layout() { + super.layout(); + setScrollPercentX(0.5f); + setScrollPercentY(0.5f); + } + + @Override + protected float getMouseWheelX() { + return 0; + } + + @Override + protected float getMouseWheelY() { + return 0; + } + }; + + VisTable overlay = new VisTable(); + overlay.align(topLeft); + overlay.pad(8); + //overlay.add(lbFrameIndex = new VisLabel()); + + VisTable controls = new VisTable(); + controls.align(bottomRight); + controls.pad(8); + controls.defaults().space(4); + controls.padBottom(controls.getPadBottom() + 18); // this is just a guess + controls.padRight(controls.getPadRight() + 18); // this is just a guess + + controls.add(new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("center"); + }}, VisUI.getSkin().getDrawable("border"), i18n("renderer-center")) {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + rendererScroller.setScrollPercentX(0.5f); + rendererScroller.setScrollPercentY(0.5f); + } + }); + }}).size(24); + + controls.add(fullscreenListener.fullscreenButton = new BorderedVisImageButton( + new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("fullscreen"); + checked = mpqViewerAssets.getDrawable("fullscreen-exit"); + }}, + VisUI.getSkin().getDrawable("border"), i18n("renderer-fullscreen")) {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + fullscreenListener.fullscreen(isChecked()); + } + }); + }}).size(24); + + rendererStack = stack(rendererScroller, controls, overlay).grow(); + }}).grow(); + }}); + setSecondWidget(new VisTable() {{ + add(controlPanel = new VisTable() {{ + // setBackground(VisUI.getSkin().getDrawable("default-pane")); + }}).grow(); + }}); + }}); + }}).grow(); + }}; + + fileTree.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + Selection selection = fileTree.getSelection(); + if (selection.isEmpty()) return; + + Node node = selection.first(); + if (node.getChildren().size > 0) { + node.setExpanded(!node.isExpanded()); + selection.remove(node); + return; + } + + for (CollapsibleVisTable o : controlPanels) o.setCollapsed(true); + + String filename = (String) fileTree.getSelectedNode().getValue(); + addressBar.setText(filename); + selectFile(selection, node, filename); + + rendererScroller.setScrollPercentX(0.5f); + rendererScroller.setScrollPercentY(0.5f); + } + }); + + // controlPanel.setDebug(true, true); + controlPanel + .align(topLeft) + .pad(4) + ; + controlPanel + .defaults() + .align(topLeft) + .growY() + // .space(4) + ; + final float controlPadding = 4; + final float labelSpacing = 4; + controlPanel.add(new VisTable() {{ + align(top); + defaults().growX(); + add(new VisTextButton("1") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + animationControls.setCollapsed(!animationControls.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("2") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + paletteControls.setCollapsed(!paletteControls.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("3") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + // audioPanel.setCollapsed(!audioPanel.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("4") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + cofControls.setCollapsed(!cofControls.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("5") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + dccControls.setCollapsed(!dccControls.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("6") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + dc6Controls.setCollapsed(!dc6Controls.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("7") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + // dt1Panel.setCollapsed(!dt1Panel.isCollapsed()); + } + }); + }}).row(); + add(new VisTextButton("8") {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + // ds1Panel.setCollapsed(!ds1Panel.isCollapsed()); + } + }); + }}).row(); + }}); + controlPanel.add(animationControls = new CollapsibleVisTable() {{ + // debug(); + add(new VisTable() {{ + add(animationControlsTabs = new TabbedPane() {{ + align(topLeft); + ANIMATION_TAB = addTab(i18n("animation"), new VisTable() {{ + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("grey")); + defaults().size(24); + add(btnFirstFrame = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("first-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("first-frame"))); + add(btnPrevFrame = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("prev-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("prev-frame"))); + add(btnPlayPause = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("play"); + checked = mpqViewerAssets.getDrawable("pause"); + }}, VisUI.getSkin().getDrawable("border"), i18n("play-pause"))); + add(btnNextFrame = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("next-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("next-frame"))); + add(btnLastFrame = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("last-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("last-frame"))); + }}).row(); + add(new VisTable() {{ + add(i18n("direction")).space(labelSpacing).growX(); + add(lbDirection = new VisLabel()).row(); + add(slDirection = new VisSlider(0, 0, 1, false) {{ + ChangeListener l; + addListener(l = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + lbDirection.setText(i18n("direction-label", getValue() + 1, getMaxValue() + 1)); + } + }); + l.changed(null, null); + }}).growX().colspan(2).row(); + add(daDirection = new DirectionActor(16)).colspan(2).row(); + }}).growX().row(); + add(new VisTable() {{ + add(i18n("frame")).space(labelSpacing).growX(); + add(lbFrameIndex = new VisLabel()).row(); + add(slFrameIndex = new VisSlider(0, 0, 1, false) {{ + ChangeListener l; + addListener(l = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + lbFrameIndex.setText(i18n("frame-label", getValue() + 1, getMaxValue() + 1)); + } + }); + l.changed(null, null); + }}).growX().colspan(2).row(); + }}).growX().row(); + add(new VisTable() {{ + add(i18n("speed")).space(labelSpacing).growX(); + add(lbFrameDuration = new VisLabel()).row(); + add(slFrameDuration = new VisSlider(0, 1024, 8, false) {{ + ChangeListener l; + addListener(l = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + lbFrameDuration.setText(i18n("speed-label", getValue())); + } + }); + l.changed(null, null); + }}).growX().colspan(2).row(); + }}).growX().row(); + add(new VisTable() {{ + add(i18n("blend")).space(labelSpacing).growX(); + add(sbBlendMode = new VisSelectBox() {{ + setItems(BlendModes.values()); + setSelectedIndex(0); + setDisabled(true); // disabled -- applied through animation + }}).row(); + }}).growX().row(); + add(new VisTable() {{ + align(topLeft); + add(cbDebugMode = new VisCheckBox(i18n("debug-bounds"), debugMode)); + }}).growX().row(); + add().growY(); + setFillParent(true); + }}); + PAGE_TAB = addTab(i18n("pages"), new VisTable() {{ + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("grey")); + defaults().size(24); + add(btnFirstPage = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("first-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("first-frame"))); + add(btnPrevPage = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("prev-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("prev-frame"))); + add(btnNextPage = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("next-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("next-frame"))); + add(btnLastPage = new BorderedVisImageButton(new VisImageButton.VisImageButtonStyle() {{ + up = mpqViewerAssets.getDrawable("last-frame"); + }}, VisUI.getSkin().getDrawable("border"), i18n("last-frame"))); + }}).row(); + add(new VisTable() {{ + add(i18n("direction")).space(labelSpacing).growX(); + add(lbDirectionPage = new VisLabel()).row(); + add(slDirectionPage = new VisSlider(0, 0, 1, false) {{ + ChangeListener l; + addListener(l = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + lbDirectionPage.setText(i18n("direction-label", getValue() + 1, getMaxValue() + 1)); + } + }); + l.changed(null, null); + }}).growX().colspan(2).row(); + }}).growX().row(); + add(new VisTable() {{ + add(i18n("page")).space(labelSpacing).growX(); + add(lbPage = new VisLabel()).row(); + add(slPage = new VisSlider(0, 0, 1, false) {{ + ChangeListener l; + addListener(l = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + lbPage.setText(i18n("page-label", getValue() + 1, getMaxValue() + 1)); + } + }); + l.changed(null, null); + }}).growX().colspan(2).row(); + }}).growX().row(); + add(new VisTable() {{ + add(i18n("blend")).space(labelSpacing).growX(); + add(sbBlendModePage = new VisSelectBox() {{ + setItems(BlendModes.values()); + setSelectedIndex(0); + setDisabled(true); // disabled -- applied through animation + }}).row(); + }}).growX().row(); + }}); + }}).pad(4).grow(); + add().growY(); + }}).growY(); + } + + @Override + public void setCollapsed(boolean collapsed) { + super.setCollapsed(collapsed); + daDirection.setVisible(!collapsed()); + }}); + controlPanel.add(paletteControls = new CollapsibleVisTable() {{ + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + pad(controlPadding); + padTop(0); // 0 on top to account for font height + // debug(); + add(i18n("palette")).align(topLeft).row(); + add(new VisTable() {{ + String[] paletteNames = new String[]{ + "ACT1", "ACT2", "ACT3", "ACT4", "ACT5", + "EndGame", "fechar", "loading", + "Menu0", "menu1", "menu2", "menu3", "menu4", + "Sky", "STATIC", "Trademark", + "Units", + }; + + palettes = new PatriciaTrie<>(); + for (String name : paletteNames) { + FileHandle handle = Gdx.files.internal("palettes/" + name + "/pal.dat"); + log.debug("Reading palette {}", handle); + ByteBuf buffer = Unpooled.wrappedBuffer(handle.readBytes()); + Palette palette = Palette.read(buffer); + palettes.put(name, palette.texture()); + } + + paletteList = new VisList<>(); + paletteList.setItems(paletteNames); + paletteList.setSelectedIndex(0); + add(paletteScroller = new VisScrollPane(paletteList) { + { + setStyle(new ScrollPaneStyle(getStyle()) {{ + vScroll = null; + vScrollKnob = mpqViewerAssets.getDrawable("vscroll"); + }}); + setBackground(VisUI.getSkin().getDrawable("default-pane")); + setFadeScrollBars(false); + setScrollingDisabled(true, false); + setForceScroll(false, true); + setOverscroll(false, false); + setScrollbarsOnTop(true); + addListener(SCROLL_ON_HOVER); + } + + @Override + protected void drawScrollBars(Batch batch, float r, float g, float b, float a) { + super.drawScrollBars(batch, r, g, b, a * 0.5f); + } + }).growY(); + }}).row(); + add().growY(); + }}).pad(4).growY(); + }}); + controlPanel.add(cofControls = new CollapsibleVisTable() {{ + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + pad(controlPadding); + padTop(0); // 0 on top to account for font height + // debug(); + add(i18n("cof")).align(topLeft).row(); + add(new VisTable() {{ + defaults().growY(); + add(new VisTable() {{ + add(i18n("triggers")).growX().row(); + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + padLeft(4); + padRight(4); + VisLabel label; + lbKeyframes = new EnumMap<>(Cof.Keyframe.class); + Cof.Keyframe[] keyframes = Cof.Keyframe.values(); + for (Cof.Keyframe keyframe : keyframes) { + lbKeyframes.put(keyframe, label = new VisLabel()); + add(keyframe.name()).spaceRight(4).left(); + add(label); + row(); + } + }}).growX().minWidth(80).row(); + add().growY(); + }}); + add(new VisTable() {{ + add(i18n("layers")).colspan(2).growX().row(); + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + components = new VisList<>(); + components.setItems(Cof.Component.values()); + components.setSelectedIndex(0); + add(componentScroller = new VisScrollPane(components) {{ + setStyle(new ScrollPaneStyle(getStyle()) {{ + vScroll = null; + vScrollKnob = mpqViewerAssets.getDrawable("vscroll"); + }}); + setBackground(VisUI.getSkin().getDrawable("default-pane")); + setFadeScrollBars(false); + setScrollingDisabled(true, false); + setForceScroll(false, true); + setOverscroll(false, false); + setScrollbarsOnTop(true); + addListener(SCROLL_ON_HOVER); + }}).minWidth(32).growY(); + }}).grow(); + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + wclasses = new VisList<>(); + add(wclassScroller = new VisScrollPane(wclasses) {{ + setStyle(new ScrollPaneStyle(getStyle()) {{ + vScroll = null; + vScrollKnob = mpqViewerAssets.getDrawable("vscroll"); + }}); + setBackground(VisUI.getSkin().getDrawable("default-pane")); + setFadeScrollBars(false); + setScrollingDisabled(true, false); + setForceScroll(false, true); + setOverscroll(false, false); + setScrollbarsOnTop(true); + addListener(SCROLL_ON_HOVER); + }}).minWidth(64).growY(); + }}).grow(); + }}).growY(); + add(new VisTable() {{ + add(cofInfo = new CofInfo()).row(); + add().growY(); + }}); + }}).grow(); + }}).pad(4).growY(); + }}); + controlPanel.add(dccControls = new CollapsibleVisTable() {{ + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + pad(controlPadding); + padTop(0); // 0 on top to account for font height + // debug(); + add(i18n("dcc")).align(topLeft).row(); + add(dccInfo = new DccInfo()).row(); + add().growY(); + }}).pad(4).growY(); + }}); + controlPanel.add(dc6Controls = new CollapsibleVisTable() {{ + add(new VisTable() {{ + setBackground(VisUI.getSkin().getDrawable("default-pane")); + pad(controlPadding); + padTop(0); // 0 on top to account for font height + // debug(); + add(i18n("dc6")).align(topLeft).row(); + add(dc6Info = new Dc6Info()).row(); + add().growY(); + }}).pad(4).growY(); + }}); + + controlPanels = new Array<>(); + controlPanels.add(animationControls); + controlPanels.add(paletteControls); + controlPanels.add(cofControls); + controlPanels.add(dccControls); + controlPanels.add(dc6Controls); + for (CollapsibleVisTable o : controlPanels) { + o.setCollapsed(true); + } + + log.debug("constructing root view..."); + root = new VisTable(); + root.add(new VisTable() {{ + final int menuPadding = 4; + setBackground(VisUI.getSkin().getDrawable("textfield")); + add(menu.getTable()).pad(menuPadding); + add(addressBar = new BorderedVisTextField() {{ + setReadOnly(true); + setDisabled(true); + setMessageText(i18n("address-hint")); + addressBarMenu = new PopupMenu() {{ + addItem(address_copy = new MenuItem(i18n("copy")) {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + Gdx.app.getClipboard().setContents(addressBar.getText()); + } + }); + }}); + addItem(address_copyFixed = new MenuItem(i18n("copy_as_path")) {{ + addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + Gdx.app.getClipboard().setContents(addressBar.getText().replace('\\', '/')); + } + }); + }}); + addItem(address_paste = new MenuItem(i18n("paste")) {{ + addListener(address_paste_clickListener = new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + if (mpqs == null) return; + + String clipboardContents = Gdx.app.getClipboard().getContents(); + if (clipboardContents == null) return; + + clipboardContents = clipboardContents.replaceAll("/", "\\\\").toUpperCase(Locale.ROOT); + Node selectedNode = fileTreeNodes.get(clipboardContents); + if (selectedNode != null) { + fileTree.collapseAll(); + selectedNode.expandTo(); + + Array children = selectedNode.getChildren(); + if (children.size > 0) { + selectedNode.setExpanded(true); + } else { + fileTree.getSelection().set(selectedNode); + } + + fileTree.layout(); + Actor actor = selectedNode.getActor(); + // fileTreeScroller.scrollTo(actor.getX(), actor.getY(), actor.getWidth(), actor.getHeight(), false, false); + } + } + }); + }}); + }}; + + addListener(new ClickListener(Input.Buttons.RIGHT) { + @Override + public void clicked(InputEvent event, float x, float y) { + addressBarMenu.showMenu(stage, event.getStageX(), event.getStageY()); + } + }); + }}).pad(menuPadding).growX(); + row(); + addSeparator().colspan(2); + }}).growX().row(); + root.add(content).grow().row(); + root.setFillParent(true); + + log.debug("constructing stage..."); + stage = new Stage(new ScreenViewport()); + stage.addActor(root); + stage.addListener(new InputListener() { + @Override + public boolean keyDown(InputEvent event, int keycode) { + if (keycode == Keys.O && UIUtils.ctrl()) { + click(file_open); + return true; + } + + return false; + } + }); + stage.addListener(new InputListener() { + @Override + public boolean keyDown(InputEvent event, int keycode) { + if (keycode == Keys.F && UIUtils.ctrl()) { + fileTreeFilter.clearText(); + fileTreeFilter.focusField(); + return true; + } + + return false; + } + }); + stage.addListener(new InputListener() { + @Override + public boolean keyDown(InputEvent event, int keycode) { + if (animationControls.collapsed()) return false; + if (animationControlsTabs.getTabIndex() != ANIMATION_TAB) return false; + if (keycode == Keys.SPACE) { + btnPlayPause.toggle(); + return true; + } else if (btnPlayPause.isChecked() && keycode == Keys.LEFT) { + click(slFrameIndex.getValue() <= slFrameIndex.getMinValue() ? btnLastFrame : btnPrevFrame); + return true; + } else if (btnPlayPause.isChecked() && keycode == Keys.RIGHT) { + click(slFrameIndex.getValue() >= slFrameIndex.getMaxValue() ? btnFirstFrame : btnNextFrame); + return true; + } + return false; + } + }); + stage.addListener(new InputListener() { + @Override + public boolean keyDown(InputEvent event, int keycode) { + if (animationControls.collapsed()) return false; + if (animationControlsTabs.getTabIndex() != PAGE_TAB) return false; + if (keycode == Keys.LEFT) { + click(slPage.getValue() <= slPage.getMinValue() ? btnLastPage : btnPrevPage); + return true; + } else if (keycode == Keys.RIGHT) { + click(slPage.getValue() >= slPage.getMaxValue() ? btnFirstPage : btnNextPage); + return true; + } + return false; + } + }); + + log.debug("setting stage as input processor..."); + Gdx.input.setInputProcessor(stage); + + ShaderProgram.pedantic = false; + Riiablo.shader = shader = new ShaderProgram( + Gdx.files.internal("shaders/indexpalette3.vert"), + Gdx.files.internal("shaders/indexpalette3.frag")); + Riiablo.batch = batch = new PaletteIndexedBatch(256, shader); + Riiablo.shapes = shapes = new ShapeRenderer(); + Riiablo.colors = new Colors(); + + reloadMpqs(); + + if (mpqs != null && initialFile != null) { + log.debug("Selecting initial file: {}", initialFile); + Gdx.app.getClipboard().setContents(initialFile); + address_paste_clickListener.clicked(null, -1, -1); + } + } + + @Override + public void dispose() { + log.debug("disposing shader..."); + shader.dispose(); + + log.debug("disposing batch..."); + batch.dispose(); + + log.debug("disposing shape renderer..."); + shader.dispose(); + + log.debug("disposing palettes..."); + for (Texture palette : palettes.values()) { + palette.dispose(); + } + + log.debug("flushing preferences..."); + prefs.flush(); + + log.debug("disposing renderer..."); + renderer.dispose(); + + log.debug("disposing stage..."); + stage.dispose(); + + log.debug("disposing VisUI..."); + VisUI.dispose(); + mpqViewerAssets.dispose(); + + log.debug("disposing asset manager..."); + assets.dispose(); + } + + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + } + + @Override + public void render() { + Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1.0f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + + assets.sync(1000 / 60L); // 1/60th of a second + stage.act(); + stage.draw(); + } + + void openMpqs() { + assert fileChooser == null; + log.debug("opening mpqs..."); + + final String home = prefs.getString("lastHome", Gdx.files.getLocalStoragePath()); + log.debug("home: {}", home); + + FileChooser fc = fileChooser = new FileChooser(OPEN); + fc.setDirectory(home); + fc.setSize(800, 600); + fc.setKeepWithinStage(false); + fc.setMultiSelectionEnabled(false); + fc.setSelectionMode(DIRECTORIES); + fc.setListener(new FileChooserAdapter() { + @Override + public void selected(Array files) { + assert files.size == 1; + FileHandle handle = files.first(); + loadMpqs(handle); + dispose(); + } + + @Override + public void canceled() { + dispose(); + } + + void dispose() { + fileChooser = null; + } + }); + + stage.addActor(fc.fadeIn()); + } + + void loadMpqs(FileHandle home) { + log.debug("loading mpqs at {}", home); + title(home.path()); + + if (assets != null) { + log.debug("disposing asset manager..."); + assets.dispose(); + assets = null; + } + + mpqs = new MpqFileResolver(home); + prefs.putString("lastHome", home.path()).flush(); + readMpqs(); + + log.debug("initializing asset manager..."); + assets = new AssetManager() + .resolver(mpqs) + .paramResolver(Dc.class, DcParams.class) + .adapter(MpqFileHandle.class, new MpqFileHandleAdapter()) + .loader(Cof.class, new CofLoader()) + .loader(Dcc.class, new DccLoader()) + .loader(Dc6.class, new Dc6Loader()) + ; + } + + void readMpqs() { + if (fileTreeNodes == null) { + fileTreeNodes = new PatriciaTrie<>(); + fileTreeCofNodes = new PatriciaTrie<>(); + } else { + fileTreeNodes.clear(); + fileTreeCofNodes.clear(); + } + + final FileHandle listfile; + listfile = Gdx.files.internal("(listfile)"); + //if (options_useExternalList.isChecked()) { + // use internal listfile + //} else { + // use external listfile or default to internal + // try { + // reader = new BufferedReader(new InputStreamReader((new ByteArrayInputStream(mpq.readBytes("(listfile)"))))); + // } catch (Throwable t) { + // reader = Gdx.files.internal(ASSETS + "(listfile)").reader(4096); + // } + //} + log.debug("listfile: {}", listfile); + + BufferedReader reader = null; + try { + reader = listfile.reader(4096); + Node root = new FileTreeNode(new VisLabel("root")); + final boolean checkExisting = options_checkExisting.isChecked(); + + log.debug("parsing listfile..."); + for (String fileName; (fileName = reader.readLine()) != null;) { + final boolean exists = mpqs.contains(fileName); + if (checkExisting && !exists) continue; + + String path = FilenameUtils.getFullPathNoEndSeparator(fileName).toUpperCase(Locale.ROOT); + treeify(fileTreeNodes, root, path); + + // hack to allow accessing files without localization metadata from mpq itself + VisLabel label = new VisLabel(FilenameUtils.getName(fileName)); + final Node node = new FileTreeNode(label); + node.setValue(fileName); + if (!exists) node.setSelectable(false); + // add listener popup + String key = fileName.toUpperCase(Locale.ROOT); + fileTreeNodes.put(key, node); + if (FilenameUtils.isExtension(key, "cof")) { + key = FilenameUtils.getBaseName("key"); + fileTreeCofNodes.put(key, node); + } + + if (path.isEmpty()) { + root.add(node); + } else { + fileTreeNodes.get(path + "\\").add(node); + } + } + + sort(root); + fileTree.clearChildren(); + for (Node child : root.getChildren()) { + fileTree.add(child); + } + + fileTree.layout(); + fileTreeFilter.clearText(); + } catch (IOException t) { + log.error("Failed to parse listfile {}", listfile, t); + } finally { + IOUtils.closeQuietly(reader); + } + } + + void reloadMpqs() { + String home = prefs.getString("lastHome"); + if (StringUtils.isNotBlank(home)) { + loadMpqs(Gdx.files.absolute(home)); + } + } + + void treeify(Trie nodes, Node root, String path) { + Node parent = root; + String[] parts = StringUtils.split(path, '\\'); + StringBuilder builder = new StringBuilder(path.length()); + for (String part : parts) { + if (part.isEmpty()) { + break; + } + + builder.append(part).append('\\'); + String partPath = builder.toString(); + Node node = nodes.get(partPath); + if (node == null) { + node = new FileTreeNode(new VisLabel(part)); + nodes.put(partPath, node); + parent.add(node); + } + + parent = node; + } + } + + void sort(Node root) { + if (root.getChildren().size == 0) return; + root.getChildren().sort(new Comparator() { + @Override + public int compare(Node o1, Node o2) { + boolean o1Empty = o1.getChildren().size == 0; + boolean o2Empty = o2.getChildren().size == 0; + if (!o1Empty && o2Empty) { + return -1; + } else if (o1Empty && !o2Empty) { + return 1; + } + + VisLabel l1 = (VisLabel) o1.getActor(); + VisLabel l2 = (VisLabel) o2.getActor(); + return StringUtils.compare( + l1.getText().toString().toUpperCase(Locale.ROOT), + l2.getText().toString().toUpperCase(Locale.ROOT)); + } + }); + + root.updateChildren(); + for (Node child : (Array) root.getChildren()) { + sort(child); + } + } + + void selectFile(Selection selection, Node node, String filename) { + log.debug("selectFile(filename: {})", filename); + final String extension = FilenameUtils.getExtension(filename).toUpperCase(Locale.ROOT); + if (extension.equals("COF")) { + animationControls.setCollapsed(false); + paletteControls.setCollapsed(false); + cofControls.setCollapsed(false); + animationControlsTabs.setDisabled(PAGE_TAB, true); + Animation anim = Animation.newAnimation(); + AssetDesc asset = AssetDesc.of(filename, Cof.class, MpqParams.of()); + AssetDesc[] layers = new AssetDesc[Cof.Component.NUM_COMPONENTS]; + assets.load(asset) + .addListener(future -> { + if (future.cause() != null) { + Dialogs + .showErrorDialog( + stage, + "Failed to load " + asset.path(), + future.cause()) + .show(stage); + return; + } + Cof cof = (Cof) future.getNow(); + cofInfo.setCof(cof); + anim.setCof(cof); + renderer.initialize(); + }); + renderer.setDrawable(new DelegatingDrawable(anim) { + @Override + protected void initialize() { + Cof cof = delegate.getCof(); + Cof.Keyframe[] keyframes = Cof.Keyframe.values(); + for (Cof.Keyframe keyframe : keyframes) { + lbKeyframes.get(keyframe).setText(cof.findKeyframe(keyframe)); + } + + String path = filename.toLowerCase(); + String name = FilenameUtils.getBaseName(path).toLowerCase(); + final String token = name.substring(0, 2); + final String mode = name.substring(2, 4); + final String wclass = name.substring(4); + final String type; + if (path.contains("monsters")) { + type = "monsters"; + } else if (path.contains("chars")) { + type = "chars"; + } else if (path.contains("objects")) { + type = "objects"; + } else { + type = "null"; + } + + if (compClasses == null) { + compClasses = new ObjectMap<>(); + for (String comp : components.getItems()) { + comp = comp.toUpperCase(Locale.ROOT); + compClasses.put(comp, Array.with("NONE")); + } + selectedWClass = new String[Cof.Component.NUM_COMPONENTS]; + } else { + for (Array a : compClasses.values()) { + a.clear(); + a.add("NONE"); + } + Arrays.fill(selectedWClass, null); + } + + for (String comp : components.getItems()) { + comp = comp.toUpperCase(Locale.ROOT); + String prefix = String.format("data\\global\\%s\\%2$s\\%3$s\\%2$s%3$s", type, token, comp).toUpperCase(Locale.ROOT); + SortedMap dcs = fileTreeNodes.prefixMap(prefix); + if (dcs.isEmpty()) { + continue; + } + + log.trace(prefix); + Array wclasses = compClasses.get(comp); + for (String dc : dcs.keySet()) { + if (!FilenameUtils.isExtension(dc, DC_EXTS)) { + continue; + } + + // TODO: hth should probably only be included if wclass doesn't exist to override it + // some reuse hth if they don't have a different animation (e.g., assassin) + if (!dc.substring(prefix.length() + 5, prefix.length() + 8).equalsIgnoreCase(wclass) + && !dc.substring(prefix.length() + 5, prefix.length() + 8).equalsIgnoreCase("HTH")) { + continue; + } + + if (!dc.substring(prefix.length() + 3, prefix.length() + 5).equalsIgnoreCase(mode)) { + continue; + } + + String clazz = dc.substring(prefix.length(), prefix.length() + 3); + if (!wclasses.contains(clazz, false)) wclasses.add(clazz); + log.trace("\t{} {}", dc, clazz); + + int l = COMP_TO_ID.get(comp, -1); + if (selectedWClass[l] == null) selectedWClass[l] = clazz; + } + } + + log.trace("selectedWClass: {}", Arrays.toString(selectedWClass)); + components.setSelectedIndex(0); + String comp = components.getSelected().toUpperCase(Locale.ROOT); + wclasses.setItems(compClasses.get(comp)); + wclasses.setSelected(selectedWClass[COMP_TO_ID.get(comp, -1)]); + + for (int l = 0; l < cof.numLayers(); l++) { + Cof.Layer layer = cof.layer(l); + + String clazz = selectedWClass[layer.component]; + if (clazz == null) continue; + + comp = components.getItems().get(layer.component); + String dcPath = String.format("data\\global\\%s\\%2$s\\%3$s\\%2$s%3$s%4$s%5$s%6$s", type, token, comp, clazz, mode, layer.weaponClass); + + for (String ext : DC_EXTS) { + final Class> dcType = ext.equals("DCC") ? Dcc.class : Dc6.class; + AssetDesc> desc = AssetDesc.of(dcPath + "." + ext, dcType, DcParams.of(0)); + if (mpqs.contains(desc.path())) { + layers[l] = desc; + assets.load(desc) + .addListener(future -> { + log.debug("Loaded {}", desc); + delegate.setLayer(layer, (Dc) future.getNow(), true); + }); + } + } + } + + slDirection.setValue(0); + slDirection.setRange(0, delegate.getNumDirections() - 1); + slDirection.fire(new ChangeEvent()); + daDirection.setDirections(delegate.getNumDirections()); + + slFrameIndex.setValue(0); + slFrameIndex.setRange(0, delegate.getNumFramesPerDir() - 1); + slFrameIndex.fire(new ChangeEvent()); + + //sbBlendMode.setSelectedIndex(0); + //cbCombineFrames.setChecked(false); + + slFrameDuration.setValue(delegate.getFrameDuration()); + delegate.setDirection((int) slDirection.getValue()); + animationControlsTabs.update(); + + String palette = paletteList.getSelected(); + Riiablo.batch.setPalette(palettes.get(palette)); + } + + @Override + public void dispose() { + super.dispose(); + assets.unload(asset); + log.debug("Unloading {}", asset); + for (AssetDesc asset : layers) { + if (asset != null) { + assets.unload(asset); + log.debug("Unloading {}", asset); + } + } + } + + void updateInfo() { + cofInfo.update(delegate.getDirection(), delegate.getFrame()); + } + + @Override + protected void changed(ChangeEvent event, Actor actor) { + if (actor == components) { + String comp = components.getSelected(); + wclasses.setItems(compClasses.get(comp)); + wclasses.setSelected(selectedWClass[COMP_TO_ID.get(comp, -1)]); + } + } + + @Override + public void draw(Batch batch, float x, float y, float width, float height) { + PaletteIndexedBatch b = Riiablo.batch; + if (!btnPlayPause.isChecked()) { + delegate.act(); + slFrameIndex.setValue(delegate.getFrame()); + updateInfo(); + } + + batch.end(); + + b.setTransformMatrix(batch.getTransformMatrix()); + b.begin(); + super.draw(b, x, y, width, height); + b.end(); + + shapes.setTransformMatrix(batch.getTransformMatrix()); + if (cbDebugMode.isChecked()) { + shapes.begin(ShapeRenderer.ShapeType.Line); + delegate.drawDebug(shapes, x, y); + shapes.end(); + } + + batch.begin(); + } + }); + } else if (extension.equals("DCC") || extension.equals("DC6")) { + animationControls.setCollapsed(false); + paletteControls.setCollapsed(false); + final Class> type = extension.equals("DCC") ? Dcc.class : Dc6.class; + (type == Dcc.class ? dccControls : dc6Controls).setCollapsed(false); + Animation anim = Animation.newAnimation(); + AssetDesc> asset = AssetDesc.of(filename, type, DcParams.of(0, -1)); + AtomicReference>> ref = new AtomicReference<>(asset); + assets.load(asset) + .addListener(future -> { + log.debug("Loaded {}", asset); + final Dc dc = (Dc) future.getNow(); + if (dc instanceof Dcc) { + Dcc dcc = (Dcc) dc; + dccInfo.setDcc(dcc); + animationControlsTabs.setDisabled(PAGE_TAB, true); + } else if (dc instanceof Dc6) { + Dc6 dc6 = (Dc6) dc; + dc6Info.setDc6(dc6); + int tabIndex = dc.direction(0).frame(0).width() >= Dc6.PAGE_SIZE + ? PAGE_TAB + : ANIMATION_TAB; + animationControlsTabs.switchTo(tabIndex); + } + + anim.edit().layer(dc).build(); + renderer.initialize(); + }); + renderer.setDrawable(new DelegatingDrawable(anim) { + boolean isAnimationTab; + int page = 0; + + { + String palette = paletteList.getSelected(); + Riiablo.batch.setPalette(palettes.get(palette)); + isAnimationTab = animationControlsTabs.getTabIndex() == ANIMATION_TAB; + } + + @Override + protected void initialize() { + Dc dc; + dc = delegate.getLayerRaw(0).getDc(); + + slDirection.setValue(0); + slDirection.setRange(0, dc.numDirections() - 1); + slDirection.fire(new ChangeEvent()); + daDirection.setDirections(dc.numDirections()); + + slFrameIndex.setValue(0); + slFrameIndex.setRange(0, dc.numFrames() - 1); + slFrameIndex.fire(new ChangeEvent()); + + //sbBlendMode.setSelectedIndex(0); + //cbCombineFrames.setChecked(false); + + slPage.setValue(0); + slPage.setRange(0, dc.numPages() - 1); + slPage.fire(new ChangeEvent()); + + slFrameDuration.setValue(256); + delegate.setDirection((int) slDirection.getValue()); + delegate.setFrameDelta((int) slFrameDuration.getValue()); + animationControlsTabs.update(); + } + + @Override + public void dispose() { + super.dispose(); + assets.unload(ref.get()); + log.debug("Unloading {}", ref.get()); + } + + void updateInfo() { + if (anim.getNumFramesPerDir() <= 0) return; + Dc dc = delegate.getLayerRaw(0).getDc(); + if (dc instanceof Dcc) { + dccInfo.update(delegate.getDirection(), delegate.getFrame()); + } else if (dc instanceof Dc6) { + dc6Info.update(delegate.getDirection(), delegate.getFrame()); + } + } + + @Override + public void switchedTab(int tabIndex) { + log.traceEntry("switchedTab(tabIndex: {})", tabIndex); + isAnimationTab = tabIndex == ANIMATION_TAB; + final AssetDesc> asset, oldAsset; + assets.unload(oldAsset = ref.get()); + ref.set(asset = AssetDesc.of(oldAsset, DcParams.of(0, isAnimationTab ? 0 : 1))); + assets.load(asset) + .addListener(future -> { + if (future.cause() != null) { + Dialogs + .showErrorDialog( + stage, + "Failed to load " + asset.path(), + future.cause()) + .show(stage); + animationControlsTabs.switchTo(isAnimationTab ? PAGE_TAB : ANIMATION_TAB); + return; + } + + log.debug("Loaded {}", asset); + final Dc dc = (Dc) future.getNow(); + if (dc instanceof Dcc) { + Dcc dcc = (Dcc) dc; + dccInfo.setDcc(dcc); + } else if (dc instanceof Dc6) { + Dc6 dc6 = (Dc6) dc; + dc6Info.setDc6(dc6); + } + + delegate.edit().layer(dc).build(); + renderer.initialize(); + }); + } + + void updateDirection(int d) { + if (delegate.getDirection() == d) return; + log.traceEntry("setDirection(d: {})", d); + final AssetDesc> oldAsset = ref.get(); + assert oldAsset.params(DcParams.class).direction != d; + final AssetDesc> asset = AssetDesc.of(oldAsset, oldAsset.params(DcParams.class).copy(d)); + ref.set(asset); + assets.load(asset) + .addListener(future -> { + if (future.cause() != null) { + Dialogs.showDetailsDialog( + stage, + "Failed to load " + asset.path(), + "Error", + ExceptionUtils.getStackTrace(future.cause()), + true) + .show(stage); + animationControlsTabs.switchTo(isAnimationTab ? PAGE_TAB : ANIMATION_TAB); + return; + } + + log.debug("Loaded {}", asset); + assets.unload(oldAsset); + final Dc dc = (Dc) future.getNow(); + if (dc instanceof Dcc) { + Dcc dcc = (Dcc) dc; + dccInfo.setDcc(dcc); + } else if (dc instanceof Dc6) { + Dc6 dc6 = (Dc6) dc; + dc6Info.setDc6(dc6); + } + + delegate.edit().layer(dc).build(); + }); + } + + @Override + protected void clicked(InputEvent event, float x, float y) { + Actor actor = event.getListenerActor(); + if (actor == btnPlayPause) { + slFrameIndex.setDisabled(!btnPlayPause.isChecked()); + } else if (actor == btnFirstFrame) { + delegate.setFrame(0); + slFrameIndex.setValue(delegate.getFrame()); + } else if (actor == btnLastFrame) { + delegate.setFrame(delegate.getNumFramesPerDir() - 1); + slFrameIndex.setValue(delegate.getFrame()); + } else if (actor == btnPrevFrame) { + int frame = delegate.getFrame(); + if (frame > 0) { + delegate.setFrame(frame - 1); + slFrameIndex.setValue(delegate.getFrame()); + } + } else if (actor == btnNextFrame) { + int frame = delegate.getFrame(); + if (frame < delegate.getNumFramesPerDir() - 1) { + delegate.setFrame(frame + 1); + slFrameIndex.setValue(delegate.getFrame()); + } + /* + } else if (actor == cbCombineFrames && cbCombineFrames.isChecked()) { + if (combined != null) combined.dispose(); + combined = dc6.render((int) slDirection.getValue(), palettes.get(paletteList + .getSelected())); + */ + } else if (actor == btnFirstPage) { + slPage.setValue(page = 0); + } else if (actor == btnLastPage) { + Dc dc = delegate.getLayerRaw(0).getDc(); + slPage.setValue(page = dc.numPages() - 1); + } else if (actor == btnPrevPage) { + if (page > 0) slPage.setValue(--page); + } else if (actor == btnNextPage) { + Dc dc = delegate.getLayerRaw(0).getDc(); + if (page < dc.numPages() - 1) slPage.setValue(++page); + } + } + + @Override + protected void changed(ChangeEvent event, Actor actor) { + if (actor == daDirection) { + int d = daDirection.getDirection(); + updateDirection(d); + delegate.setDirection(d); + slDirection.setValue(d); + } else if (actor == slDirection) { + int d = (int) slDirection.getValue(); + updateDirection(d); + delegate.setDirection(d); + updateInfo(); + } else if (actor == paletteList) { + String palette = paletteList.getSelected(); + Riiablo.batch.setPalette(palettes.get(palette)); + log.debug("palette -> {}", palette); + /*} else if (actor == sbBlendMode) { + int frame = delegate.getFrame(); + //if (pages != null) { + // for (int p = 0; p < pages.size; p++) pages.get(p).dispose(); + // pages = new DC6.PageList(dc6.pages((int) slDirectionPage.getValue(), palettes.get(paletteList.getSelected()), sbBlendModePage.getSelected())); + //} + if (combined != null) { + combined.dispose(); + combined = dc6.render((int) slDirection.getValue(), palettes.get(paletteList.getSelected()), sbBlendMode.getSelected()); + }*/ + } else if (actor == slFrameIndex) { + delegate.setFrame((int) slFrameIndex.getValue()); + updateInfo(); + } else if (actor == slFrameDuration) { + //delegate.setFrameDuration(1 / slFrameDuration.getValue()); + delegate.setFrameDelta((int) slFrameDuration.getValue()); + } else if (actor == slPage) { + //delegate.setFrame((int) slPage.getValue()); + //} else if (actor == slDirectionPage || /*actor == sbBlendModePage || */(actor == paletteList && pages != null)) { + //for (int p = 0; p < pages.size; p++) pages.get(p).dispose(); + //pages = new DC6.PageList(dc6.pages((int) slDirectionPage.getValue(), palettes.get(paletteList.getSelected()), sbBlendModePage.getSelected())); + } + } + + @Override + public void draw(Batch batch, float x, float y, float width, float height) { + if (delegate.getLayerRaw(0) == null) return; + Dc dc = delegate.getLayerRaw(0).getDc(); + PaletteIndexedBatch b = Riiablo.batch; + if (!isAnimationTab && dc != null) { + batch.end(); + + b.setTransformMatrix(batch.getTransformMatrix()); + b.begin(); + TextureRegion page = dc.page((int) slDirectionPage.getValue(), (int) slPage.getValue()); + + b.draw(page, x - (page.getRegionWidth() / 2f), y - (page.getRegionHeight() / 2f)); + b.end(); + + batch.begin(); + return; + } + + if (!btnPlayPause.isChecked()) { + delegate.act(); + slFrameIndex.setValue(delegate.getFrame()); + updateInfo(); + } + + batch.end(); + + b.setTransformMatrix(batch.getTransformMatrix()); + b.begin(); + super.draw(b, x, y, width, height); + b.end(); + + shapes.setTransformMatrix(batch.getTransformMatrix()); + if (cbDebugMode.isChecked()) { + shapes.begin(ShapeRenderer.ShapeType.Line); + delegate.drawDebug(shapes, x, y); + shapes.end(); + } + + batch.begin(); + } + }); + } + } + + static final class FileTreeNode extends Node { + FileTreeNode(Actor actor) { + super(actor); + } + } + + final class FullscreenListener extends ClickListener { + float verticalSplitAmount; + float horizontalSplitAmount; + Button fullscreenButton; + + boolean filled = false; + + @Override + public void clicked(InputEvent event, float x, float y) { + final int tapCount = getTapCount(); + if (tapCount >= 2 && tapCount % 2 == 0) { + fullscreen(!filled); + } + } + + void fullscreen(boolean b) { + filled = b; + fullscreenButton.setChecked(filled); + if (filled) { + verticalSplitAmount = verticalSplit.getSplit(); + horizontalSplitAmount = horizontalSplit.getSplit(); + verticalSplit.setSplitAmount(0); + horizontalSplit.setSplitAmount(1); + } else { + verticalSplit.setSplitAmount(verticalSplitAmount); + horizontalSplit.setSplitAmount(horizontalSplitAmount); + } + } + } + + enum BlendModes { + NONE(BlendMode.NONE), + ID(BlendMode.ID), + LUMINOSITY(BlendMode.LUMINOSITY), + LUMINOSITY_TINT(BlendMode.LUMINOSITY_TINT), + SOLID(BlendMode.SOLID), + TINT_BLACKS(BlendMode.TINT_BLACKS), + TINT_WHITES(BlendMode.TINT_WHITES), + TINT_ID(BlendMode.TINT_ID), + BRIGHTEN(BlendMode.BRIGHTEN), + TINT_ID_RED(BlendMode.TINT_ID_RED), + DARKEN(BlendMode.DARKEN), + ; + + final int value; + BlendModes(int value) { + this.value = value; + } + } + + static final class Renderer extends Actor implements Disposable { + final Color backgroundColor = Color.BLACK.cpy(); + Texture background; + Drawable drawable; + + Renderer() { + setSize(2048, 2048); + updateBackground(); + } + + @Override + public void dispose() { + AssetUtils.dispose(background); + disposeDrawable(); + } + + void disposeDrawable() { + AssetUtils.dispose(drawable); + if (drawable instanceof TextureRegionDrawable) { + ((TextureRegionDrawable) drawable).getRegion().getTexture().dispose(); + } + } + + protected void initialize() { + if (drawable instanceof DelegatingDrawable) ((DelegatingDrawable) drawable).initialize(); + } + + public Color getBackground() { + return backgroundColor; + } + + public void setBackground(Color color) { + if (backgroundColor.equals(color)) return; + backgroundColor.set(color); + updateBackground(); + } + + void updateBackground() { + if (background == null) background = new Texture(1, 1, Pixmap.Format.RGBA8888); + Pixmap p = new Pixmap(1, 1, Pixmap.Format.RGBA8888); + try { + p.setColor(backgroundColor); + p.drawPixel(0, 0); + background.draw(p, 0, 0); + } finally { + p.dispose(); + } + } + + public void setDrawable(Drawable drawable) { + if (Objects.equals(drawable, this.drawable)) return; + disposeDrawable(); + this.drawable = drawable; + } + + @Override + public void draw(Batch batch, float a) { + batch.draw(background, 0, 0, getWidth(), getHeight()); + if (drawable != null) drawDelegate(batch, a); + } + + protected void drawDelegate(Batch batch, float a) { + /*ShaderProgram s = null; + if (shader != null && palette != null) { + batch.flush(); + s = batch.getShader(); + batch.setShader(shader); + + palette.bind(1); + Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0); + shader.setUniformi("ColorTable", 1); + }*/ + + drawable.draw(batch, + getX(center) - (drawable.getMinWidth() / 2), + getY(center) - (drawable.getMinHeight() / 2), + drawable.getMinWidth(), drawable.getMinHeight()); + + /*if (shader != null && palette != null) { + batch.setShader(s); + }*/ + } + + @Override + public void drawDebug(ShapeRenderer shapes) { + //drawDebugOrigin(shapes); + super.drawDebug(shapes); + } + } + + public class DelegatingDrawable extends BaseDrawable implements Disposable, TabbedPane.TabListener { + protected T delegate; + protected ClickListener clickListener = new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + DelegatingDrawable.this.clicked(event, x, y); + } + }; + protected ChangeListener changeListener = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + DelegatingDrawable.this.changed(event, actor); + } + }; + + public DelegatingDrawable() { + animationControlsTabs.addListener(this); + animationControlsTabs.setDisabled(PAGE_TAB, false); + animationControlsTabs.setDisabled(ANIMATION_TAB, false); + btnPlayPause .addListener(clickListener); + btnFirstFrame .addListener(clickListener); + btnLastFrame .addListener(clickListener); + btnPrevFrame .addListener(clickListener); + btnNextFrame .addListener(clickListener); + paletteList .addListener(changeListener); + daDirection .addListener(changeListener); + slDirection .addListener(changeListener); + slFrameIndex .addListener(changeListener); + slFrameDuration .addListener(changeListener); + // sbBlendMode .addListener(changeListener); + //cbCombineFrames.addListener(sbBlendModePage); + // btnPlayPauseAudio.addListener(clickListener); + // btnRestartAudio .addListener(clickListener); + // slAudioScrubber .addListener(changeListener); + // slVolume .addListener(changeListener); + animationControls.addListener(changeListener); + slPage .addListener(changeListener); + btnFirstPage .addListener(clickListener); + btnLastPage .addListener(clickListener); + btnPrevPage .addListener(clickListener); + btnNextPage .addListener(clickListener); + slDirectionPage .addListener(changeListener); + // sbBlendModePage .addListener(changeListener); + components .addListener(changeListener); + wclasses .addListener(changeListener); + } + + public DelegatingDrawable(T delegate) { + this(); + this.delegate = delegate; + } + + protected void initialize() {} + + @Override + public void dispose() { + if (delegate instanceof Disposable) ((Disposable) delegate).dispose(); + animationControlsTabs.removeListener(this); + btnPlayPause .removeListener(clickListener); + btnFirstFrame .removeListener(clickListener); + btnLastFrame .removeListener(clickListener); + btnPrevFrame .removeListener(clickListener); + btnNextFrame .removeListener(clickListener); + paletteList .removeListener(changeListener); + daDirection .removeListener(changeListener); + slDirection .removeListener(changeListener); + slFrameIndex .removeListener(changeListener); + slFrameDuration .removeListener(changeListener); + //sbBlendMode .removeListener(changeListener); + //cbCombineFrames.removeListener(clickListener); + // btnPlayPauseAudio.removeListener(clickListener); + // btnRestartAudio .removeListener(clickListener); + // slAudioScrubber .removeListener(changeListener); + // slVolume .removeListener(changeListener); + animationControls.removeListener(changeListener); + slPage .removeListener(changeListener); + btnFirstPage .removeListener(clickListener); + btnLastPage .removeListener(clickListener); + btnPrevPage .removeListener(clickListener); + btnNextPage .removeListener(clickListener); + slDirectionPage .removeListener(changeListener); + //sbBlendModePage .removeListener(changeListener); + components .removeListener(changeListener); + wclasses .removeListener(changeListener); + } + + protected void clicked(InputEvent event, float x, float y) {} + + protected void changed(ChangeEvent event, Actor actor) {} + + @Override + public void switchedTab(int tabIndex) {} + + public void setDelegate(T drawable) { + if (Objects.equals(drawable, delegate)) { + return; + } + + if (delegate instanceof Disposable) ((Disposable) delegate).dispose(); + delegate = drawable; + } + + @Override + public void draw(Batch batch, float x, float y, float width, float height) { + if (delegate != null) delegate.draw(batch, x, y, width, height); + } + } +} diff --git a/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/BorderedVisImageButton.java b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/BorderedVisImageButton.java new file mode 100644 index 00000000..ba04c8e3 --- /dev/null +++ b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/BorderedVisImageButton.java @@ -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()); + } + } +} diff --git a/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/BorderedVisTextField.java b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/BorderedVisTextField.java new file mode 100644 index 00000000..3a14d33e --- /dev/null +++ b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/BorderedVisTextField.java @@ -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()); + } + } +} diff --git a/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/ButtonGroup.java b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/ButtonGroup.java new file mode 100644 index 00000000..a10fe8b1 --- /dev/null +++ b/tools/mpq-viewer/src/main/java/com/riiablo/tool/mpqviewer/widget/ButtonGroup.java @@ -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 extends com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup { + final IntMap