// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.graphics;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.key.FileResourceEntry;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.util.io.FileManager;
import org.infinity.util.io.StreamUtils;
/**
* Handles BAM v2 resources.
*/
public class BamV2Decoder extends BamDecoder
{
private final List<BamV2FrameEntry> listFrames = new ArrayList<BamV2FrameEntry>();
private final List<CycleEntry> listCycles = new ArrayList<CycleEntry>();
private final BamV2FrameEntry defaultFrameInfo = new BamV2FrameEntry(null, 0, 0);
private BamV2Control defaultControl;
private ByteBuffer bamBuffer; // contains the raw (uncompressed) BAM v2 data
private Path bamPath; // base path of the BAM resource (or null if BAM is biffed)
private int numDataBlocks; // number of PVRZ data blocks
public BamV2Decoder(ResourceEntry bamEntry)
{
super(bamEntry);
init();
}
@Override
public BamV2Control createControl()
{
return new BamV2Control(this);
}
@Override
public BamV2FrameEntry getFrameInfo(int frameIdx)
{
if (frameIdx >= 0 && frameIdx < listFrames.size()) {
return listFrames.get(frameIdx);
} else {
return defaultFrameInfo;
}
}
@Override
public void close()
{
PvrDecoder.flushCache();
bamBuffer = null;
listFrames.clear();
listCycles.clear();
}
@Override
public boolean isOpen()
{
return (bamBuffer != null);
}
@Override
public void reload()
{
init();
}
@Override
public ByteBuffer getResourceBuffer()
{
return bamBuffer;
}
@Override
public int frameCount()
{
return listFrames.size();
}
@Override
public Image frameGet(BamControl control, int frameIdx)
{
if (frameIdx >= 0 && frameIdx < listFrames.size()) {
if (control == null) {
control = defaultControl;
}
int w, h;
if (control.getMode() == BamDecoder.BamControl.Mode.SHARED) {
Dimension d = control.getSharedDimension();
w = d.width;
h = d.height;
} else {
w = getFrameInfo(frameIdx).getWidth();
h = getFrameInfo(frameIdx).getHeight();
}
if (w > 0 && h > 0) {
BufferedImage image = ColorConvert.createCompatibleImage(w, h, true);
frameGet(control, frameIdx, image);
return image;
}
}
return ColorConvert.createCompatibleImage(1, 1, true);
}
@Override
public void frameGet(BamControl control, int frameIdx, Image canvas)
{
if (canvas != null && frameIdx >= 0 && frameIdx < listFrames.size()) {
if(control == null) {
control = defaultControl;
}
int w, h;
if (control.getMode() == BamDecoder.BamControl.Mode.SHARED) {
Dimension d = control.getSharedDimension();
w = d.width;
h = d.height;
} else {
w = getFrameInfo(frameIdx).getWidth();
h = getFrameInfo(frameIdx).getHeight();
}
if (w > 0 && h > 0 && canvas.getWidth(null) >= w && canvas.getHeight(null) >= h) {
renderFrame(control, frameIdx, canvas);
}
}
}
/** Returns the number of PVRZ data blocks referred to in this BAM. */
public int getDataBlockCount()
{
return numDataBlocks;
}
private void init()
{
// resetting data
close();
ResourceEntry entry = getResourceEntry();
if (entry != null) {
try {
Path bamFile = entry.getActualPath();
if (bamFile != null) {
bamPath = bamFile.getParent();
// Skip path if it denotes an override folder of the game
List<Path> list = Profile.getOverrideFolders(true);
if (list != null) {
for (final Path path: list) {
if (bamPath.equals(path)) {
bamPath = null;
break;
}
}
}
}
bamBuffer = entry.getResourceBuffer();
String signature = StreamUtils.readString(bamBuffer, 0, 4);
String version = StreamUtils.readString(bamBuffer, 4, 4);
if (!"BAM ".equals(signature) || !"V2 ".equals(version)) {
throw new Exception("Invalid BAM type");
}
setType(Type.BAMV2);
// evaluating header data
int framesCount = bamBuffer.getInt(8);
if (framesCount <= 0) {
throw new Exception("Invalid number of frames");
}
int cyclesCount = bamBuffer.getInt(0x0c);
if (cyclesCount <= 0) {
throw new Exception("Invalid number of cycles");
}
numDataBlocks = bamBuffer.getInt(0x10);
if (numDataBlocks <= 0) {
throw new Exception("Invalid number of data blocks");
}
int ofsFrames = bamBuffer.getInt(0x14);
if (ofsFrames < 0x20) {
throw new Exception("Invalid frames offset");
}
int ofsCycles = bamBuffer.getInt(0x18);
if (ofsCycles < 0x20) {
throw new Exception("Invalid cycles offset");
}
int ofsBlocks = bamBuffer.getInt(0x1c);
if (ofsBlocks < 0x20) {
throw new Exception("Invalid data blocks offset");
}
int ofs = ofsFrames;
// processing frame entries
for (int i = 0; i < framesCount; i++) {
listFrames.add(new BamV2FrameEntry(bamBuffer, ofs, ofsBlocks));
ofs += 0x0c;
}
// processing cycle entries
ofs = ofsCycles;
for (int i = 0; i < cyclesCount; i++) {
int cnt = bamBuffer.getShort(ofs) & 0xffff;
int idx = bamBuffer.getShort(ofs+2) & 0xffff;
listCycles.add(new CycleEntry(idx, cnt));
ofs += 4;
}
// creating default bam control instance as a fallback option
defaultControl = new BamV2Control(this);
defaultControl.setMode(BamControl.Mode.SHARED);
defaultControl.setSharedPerCycle(false);
} catch (Exception e) {
e.printStackTrace();
close();
}
}
}
// Returns and caches the PVRZ resource of the specified page
private PvrDecoder getPVR(int page)
{
try {
String name = String.format("MOS%1$04d.PVRZ", page);
ResourceEntry entry = null;
if (bamPath != null) {
// preferring PVRZ files from the BAM's base path
Path pvrzFile = FileManager.resolve(bamPath.resolve(name));
if (Files.isRegularFile(pvrzFile)) {
entry = new FileResourceEntry(pvrzFile);
}
}
if (entry == null) {
// fallback: use PVRZ resources from game
entry = ResourceFactory.getResourceEntry(name);
}
if (entry != null) {
return PvrDecoder.loadPvr(entry);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// Draws the absolute frame onto the canvas. Takes BAM mode into account.
private void renderFrame(BamControl control, int frameIdx, Image canvas)
{
if (canvas != null && frameIdx >= 0 && frameIdx < listFrames.size()) {
if (control == null) {
control = defaultControl;
}
// decoding frame data
BufferedImage image = ColorConvert.toBufferedImage(canvas, true, true);
int dstWidth = image.getWidth();
int dstHeight = image.getHeight();
int[] dstBuffer = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
int srcWidth = listFrames.get(frameIdx).width;
int srcHeight = listFrames.get(frameIdx).height;
int[] srcBuffer = ((DataBufferInt)listFrames.get(frameIdx).frame.getRaster().getDataBuffer()).getData();
if (control.getMode() == BamControl.Mode.SHARED) {
// drawing on shared canvas
int left = -control.getSharedRectangle().x - listFrames.get(frameIdx).centerX;
int top = -control.getSharedRectangle().y - listFrames.get(frameIdx).centerY;
int maxWidth = (dstWidth < srcWidth + left) ? dstWidth : srcWidth;
int maxHeight = (dstHeight < srcHeight + top) ? dstHeight : srcHeight;
int srcOfs = 0, dstOfs = top*dstWidth + left;
for (int y = 0; y < maxHeight; y++) {
for (int x = 0; x < maxWidth; x++) {
dstBuffer[dstOfs+x] = srcBuffer[srcOfs+x];
}
srcOfs += srcWidth;
dstOfs += dstWidth;
}
} else {
// drawing on individual canvas
int srcOfs = 0, dstOfs = 0;
int maxWidth = (dstWidth < srcWidth) ? dstWidth : srcWidth;
int maxHeight = (dstHeight < srcHeight) ? dstHeight : srcHeight;
for (int y = 0; y < maxHeight; y++) {
for (int x = 0; x < maxWidth; x++) {
dstBuffer[dstOfs+x] = srcBuffer[srcOfs+x];
}
srcOfs += srcWidth;
dstOfs += dstWidth;
}
}
dstBuffer = null;
// rendering resulting image onto the canvas if needed
if (image != canvas) {
Graphics g = canvas.getGraphics();
try {
g.drawImage(image, 0, 0, null);
} finally {
g.dispose();
g = null;
}
image.flush();
image = null;
}
}
}
//-------------------------- INNER CLASSES --------------------------
// Stores information for a single frame entry
public class BamV2FrameEntry implements BamDecoder.FrameEntry
{
private final int dataBlockSize = 0x1c; // size of a single data block
private int width, height, centerX, centerY;
private BufferedImage frame;
private BamV2FrameEntry(ByteBuffer buffer, int ofsFrame, int ofsBlocks)
{
if (buffer != null && ofsFrame < buffer.limit() && ofsBlocks < buffer.limit()) {
width = buffer.getShort(ofsFrame) & 0xffff;
height = buffer.getShort(ofsFrame+2) & 0xffff;
centerX = buffer.getShort(ofsFrame+4);
centerY = buffer.getShort(ofsFrame+6);
int blockStart = buffer.getShort(ofsFrame+8) & 0xffff;
int blockCount = buffer.getShort(ofsFrame+10) & 0xffff;
decodeImage(buffer, ofsBlocks, blockStart, blockCount);
} else {
width = height = centerX = centerY = 0;
frame = null;
}
}
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return height; }
@Override
public int getCenterX() { return centerX; }
@Override
public int getCenterY() { return centerY; }
public Image getImage() { return frame; }
private void decodeImage(ByteBuffer buffer, int ofsBlocks, int start, int count)
{
frame = null;
if (width > 0 && height > 0) {
frame = ColorConvert.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
int ofs = ofsBlocks + start*dataBlockSize;
for (int i = 0; i < count; i++) {
int page = buffer.getInt(ofs);
int srcX = buffer.getInt(ofs+0x04);
int srcY = buffer.getInt(ofs+0x08);
int w = buffer.getInt(ofs+0x0c);
int h = buffer.getInt(ofs+0x10);
int dstX = buffer.getInt(ofs+0x14);
int dstY = buffer.getInt(ofs+0x18);
ofs += dataBlockSize;
PvrDecoder decoder = getPVR(page);
if (decoder != null) {
try {
BufferedImage srcImage = decoder.decode(srcX, srcY, w, h);
Graphics g = frame.getGraphics();
g.drawImage(srcImage, dstX, dstY, null);
g.dispose();
g = null;
decoder = null;
srcImage = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
/** Provides access to cycle-specific functionality. */
public static class BamV2Control extends BamControl
{
private int currentCycle, currentFrame;
protected BamV2Control(BamV2Decoder decoder)
{
super(decoder);
init();
}
@Override
public BamV2Decoder getDecoder()
{
return (BamV2Decoder)super.getDecoder();
}
@Override
public int cycleCount()
{
return getDecoder().listCycles.size();
}
@Override
public int cycleFrameCount()
{
if (currentCycle < getDecoder().listCycles.size()) {
return getDecoder().listCycles.get(currentCycle).framesCount;
} else {
return 0;
}
}
@Override
public int cycleFrameCount(int cycleIdx)
{
if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) {
return getDecoder().listCycles.get(cycleIdx).framesCount;
} else {
return 0;
}
}
@Override
public int cycleGet()
{
return currentCycle;
}
@Override
public boolean cycleSet(int cycleIdx)
{
if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size() && currentCycle != cycleIdx) {
currentCycle = cycleIdx;
if (isSharedPerCycle()) {
updateSharedBamSize();
}
return true;
} else {
return false;
}
}
@Override
public boolean cycleHasNextFrame()
{
if (currentCycle < getDecoder().listCycles.size()) {
return (currentFrame < getDecoder().listCycles.get(currentCycle).framesCount - 1);
} else {
return false;
}
}
@Override
public boolean cycleNextFrame()
{
if (cycleHasNextFrame()) {
currentFrame++;
return true;
} else {
return false;
}
}
@Override
public void cycleReset()
{
currentFrame = 0;
}
@Override
public Image cycleGetFrame()
{
int frameIdx = cycleGetFrameIndexAbsolute();
return getDecoder().frameGet(this, frameIdx);
}
@Override
public void cycleGetFrame(Image canvas)
{
int frameIdx = cycleGetFrameIndexAbsolute();
getDecoder().frameGet(this, frameIdx, canvas);
}
@Override
public Image cycleGetFrame(int frameIdx)
{
frameIdx = cycleGetFrameIndexAbsolute(frameIdx);
return getDecoder().frameGet(this, frameIdx);
}
@Override
public void cycleGetFrame(int frameIdx, Image canvas)
{
frameIdx = cycleGetFrameIndexAbsolute(frameIdx);
getDecoder().frameGet(this, frameIdx, canvas);
}
@Override
public int cycleGetFrameIndex()
{
return currentFrame;
}
@Override
public boolean cycleSetFrameIndex(int frameIdx)
{
if (currentCycle < getDecoder().listCycles.size() &&
frameIdx >= 0 && frameIdx < getDecoder().listCycles.get(currentCycle).framesCount) {
currentFrame = frameIdx;
return true;
} else {
return false;
}
}
@Override
public int cycleGetFrameIndexAbsolute()
{
return cycleGetFrameIndexAbsolute(currentCycle, currentFrame);
}
@Override
public int cycleGetFrameIndexAbsolute(int frameIdx)
{
return cycleGetFrameIndexAbsolute(currentCycle, frameIdx);
}
@Override
public int cycleGetFrameIndexAbsolute(int cycleIdx, int frameIdx)
{
if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size() &&
frameIdx >= 0 && frameIdx < getDecoder().listCycles.get(cycleIdx).framesCount) {
return getDecoder().listCycles.get(cycleIdx).startIndex + frameIdx;
} else {
return -1;
}
}
private void init()
{
currentCycle = currentFrame = 0;
updateSharedBamSize();
}
}
// Stores information for a single cycle
private class CycleEntry
{
public int startIndex, framesCount;
public CycleEntry(int startIndex, int framesCount)
{
if (startIndex >= 0 && framesCount > 0) {
this.startIndex = startIndex;
this.framesCount = framesCount;
} else {
this.startIndex = this.framesCount = 0;
}
}
}
}