// 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.key;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import org.infinity.gui.BIFFEditor;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.util.io.FileManager;
import org.infinity.util.io.StreamUtils;
public final class BIFFWriter
{
private final BIFFEntry bifEntry;
private final Map<ResourceEntry, Boolean> resources = new HashMap<ResourceEntry, Boolean>();
private final Map<ResourceEntry, Boolean> tileResources = new HashMap<ResourceEntry, Boolean>();
private final int format;
private static byte[] compress(byte data[])
{
Deflater deflater = new Deflater();
byte compr[] = new byte[data.length * 2];
deflater.setInput(data);
deflater.finish();
int clength = deflater.deflate(compr);
return Arrays.copyOfRange(compr, 0, clength);
}
private static void compressBIF(Path biff, Path compr, String uncrfilename) throws IOException
{
try (OutputStream os = StreamUtils.getOutputStream(compr, true)) {
StreamUtils.writeString(os, "BIF ", 4);
StreamUtils.writeString(os, "V1.0", 4);
StreamUtils.writeInt(os, uncrfilename.length());
StreamUtils.writeString(os, uncrfilename, uncrfilename.length());
StreamUtils.writeInt(os, (int)Files.size(biff)); // Uncompressed length
StreamUtils.writeInt(os, 0); // Compressed length
try (OutputStream dos = new DeflaterOutputStream(os)) {
try (InputStream is = StreamUtils.getInputStream(biff)) {
byte[] buffer = new byte[32765];
int bytesread = is.read(buffer, 0, buffer.length);
while (bytesread != -1) {
dos.write(buffer, 0, bytesread);
bytesread = is.read(buffer, 0, buffer.length);
}
}
}
}
int comprsize = (int)(Files.size(compr)) - (0x20 + uncrfilename.length());
try (FileChannel ch = FileChannel.open(compr, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
ch.position((long)(0x10 + uncrfilename.length()));
StreamUtils.writeInt(ch, comprsize);
}
}
private static void compressBIFC(Path biff, Path compr) throws Exception
{
try (OutputStream os = StreamUtils.getOutputStream(compr, true)) {
StreamUtils.writeString(os, "BIFC", 4);
StreamUtils.writeString(os, "V1.0", 4);
StreamUtils.writeInt(os, (int)Files.size(biff));
try (InputStream is = StreamUtils.getInputStream(biff)) {
byte block[] = readBytes(is, 8192);
while (block.length != 0) {
byte[] compressed = compress(block);
StreamUtils.writeInt(os, block.length);
StreamUtils.writeInt(os, compressed.length);
StreamUtils.writeBytes(os, compressed);
block = readBytes(is, 8192);
}
}
}
}
private static byte[] readBytes(InputStream is, int length) throws Exception
{
byte[] buffer = new byte[length];
int bytesread = 0;
while (bytesread < length) {
int newread = is.read(buffer, bytesread, length - bytesread);
if (newread == -1) {
break;
}
bytesread += newread;
}
return Arrays.copyOfRange(buffer, 0, bytesread);
}
public BIFFWriter(BIFFEntry bifEntry, int format)
{
this.bifEntry = bifEntry;
this.format = format;
if (bifEntry.getIndex() == -1) { // new biff-file
ResourceFactory.getKeyfile().addBIFFEntry(bifEntry);
}
}
public void addResource(ResourceEntry resourceEntry, boolean ignoreoverride)
{
if (resourceEntry.getExtension().equalsIgnoreCase("TIS")) {
tileResources.put(resourceEntry, Boolean.valueOf(ignoreoverride));
} else {
resources.put(resourceEntry, Boolean.valueOf(ignoreoverride));
}
}
public void write() throws Exception
{
Path biffPath = FileManager.query(Profile.getGameRoot(), "data");
if (biffPath == null || !Files.isDirectory(biffPath)) {
throw new Exception("No BIFF folder found.");
}
Path dummyFile = Files.createTempFile(biffPath, "_dummy", ".bif");
Path compressedFile = null;
try {
writeBIFF(dummyFile);
ResourceFactory.getKeyfile().closeBIFFFiles();
bifEntry.setFileSize((int)Files.size(dummyFile)); // Uncompressed length
if (format == BIFFEditor.BIFF) {
// Delete old BIFF, rename this to real name
Path realFile = bifEntry.getPath();
if (realFile == null) {
realFile = FileManager.query(Profile.getGameRoot(), bifEntry.toString());
}
if (Files.isRegularFile(realFile)) {
Files.delete(realFile);
}
Files.move(dummyFile, realFile);
} else if (format == BIFFEditor.BIF) {
compressedFile = Files.createTempFile(biffPath, "_dummy", ".cbf");
compressBIF(dummyFile, compressedFile, bifEntry.toString());
Files.delete(dummyFile);
// Delete old BIFF, rename this to real name
Path realFile = bifEntry.getPath();
if (realFile == null) {
realFile = FileManager.query(Profile.getGameRoot(), bifEntry.toString());
}
if (Files.isRegularFile(realFile)) {
Files.delete(realFile);
}
Files.move(compressedFile, realFile);
} else if (format == BIFFEditor.BIFC) {
compressedFile = Files.createTempFile(biffPath, "_dummy", ".bif");
compressBIFC(dummyFile, compressedFile);
Files.delete(dummyFile);
// Delete old BIFF, rename this to real name
Path realFile = bifEntry.getPath();
if (realFile == null) {
realFile = FileManager.query(Profile.getRootFolders(), bifEntry.toString());
}
if (Files.isRegularFile(realFile)) {
Files.delete(realFile);
}
Files.move(compressedFile, realFile);
}
} finally {
if (dummyFile != null && Files.isRegularFile(dummyFile)) {
try {
Files.delete(dummyFile);
} catch (IOException e) {
}
}
if (compressedFile != null && Files.isRegularFile(compressedFile)) {
try {
Files.delete(compressedFile);
} catch (IOException e) {
}
}
}
}
private BIFFResourceEntry reloadNode(ResourceEntry entry, int newOffset)
{
ResourceFactory.getResources().removeResourceEntry(entry);
BIFFResourceEntry newEntry = new BIFFResourceEntry(bifEntry, entry.toString(), newOffset);
ResourceFactory.getResources().addResourceEntry(newEntry, newEntry.getTreeFolder(), true);
return newEntry;
}
private void writeBIFF(Path file) throws Exception
{
try (OutputStream os = StreamUtils.getOutputStream(file, true)) {
StreamUtils.writeString(os, "BIFF", 4);
StreamUtils.writeString(os, "V1 ", 4);
StreamUtils.writeInt(os, resources.size());
StreamUtils.writeInt(os, tileResources.size());
StreamUtils.writeInt(os, 0x14);
int offset = 20 + 16 * resources.size() + 20 * tileResources.size();
int index = 0; // Non-tileset index starts at 0
for (final ResourceEntry resourceEntry : resources.keySet()) {
BIFFResourceEntry newentry = reloadNode(resourceEntry, index);
StreamUtils.writeInt(os, newentry.getLocator());
StreamUtils.writeInt(os, offset); // Offset
int info[] = resourceEntry.getResourceInfo(resources.get(resourceEntry).booleanValue());
offset += info[0];
StreamUtils.writeInt(os, info[0]); // Size
StreamUtils.writeShort(os, (short)ResourceFactory.getKeyfile().getExtensionType(resourceEntry.getExtension()));
StreamUtils.writeShort(os, (short)0); // Unknown
index++;
}
index = 1; // Tileset index starts at 1
for (final ResourceEntry resourceEntry : tileResources.keySet()) {
BIFFResourceEntry newentry = reloadNode(resourceEntry, index);
StreamUtils.writeInt(os, newentry.getLocator());
StreamUtils.writeInt(os, offset); // Offset
int info[] = resourceEntry.getResourceInfo(tileResources.get(resourceEntry).booleanValue());
StreamUtils.writeInt(os, info[0]); // Number of tiles
StreamUtils.writeInt(os, info[1]); // Size of each tile (in bytes)
offset += info[0] * info[1];
StreamUtils.writeShort(os, (short)ResourceFactory.getKeyfile().getExtensionType(resourceEntry.getExtension()));
StreamUtils.writeShort(os, (short)0); // Unknown
index++;
}
for (final ResourceEntry resourceEntry : resources.keySet()) {
StreamUtils.writeBytes(os, resourceEntry.getResourceBuffer(resources.get(resourceEntry).booleanValue()));
}
for (final ResourceEntry resourceEntry : tileResources.keySet()) {
ByteBuffer buffer = resourceEntry.getResourceBuffer(tileResources.get(resourceEntry).booleanValue());
int info[] = resourceEntry.getResourceInfo(tileResources.get(resourceEntry).booleanValue());
int size = info[0]*info[1];
int toSkip = buffer.limit() - size;
if (toSkip > 0) {
buffer.position(toSkip); // skipping TIS header
}
StreamUtils.writeBytes(os, buffer);
}
}
}
}