// 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.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.ImageIcon;
import javax.swing.SwingUtilities;
import org.infinity.icon.Icons;
import org.infinity.resource.ResourceFactory;
import org.infinity.util.IntegerHashMap;
import org.infinity.util.Misc;
import org.infinity.util.io.StreamUtils;
public class Keyfile
{
public static final ImageIcon ICON_STRUCT = Icons.getIcon(Icons.ICON_ROW_INSERT_AFTER_16);
public static final int TYPE_BMP = 0x001;
public static final int TYPE_MVE = 0x002;
public static final int TYPE_WAV = 0x004;
public static final int TYPE_WFX = 0x005;
public static final int TYPE_PLT = 0x006;
public static final int TYPE_BAM = 0x3e8;
public static final int TYPE_WED = 0x3e9;
public static final int TYPE_CHU = 0x3ea;
public static final int TYPE_TIS = 0x3eb;
public static final int TYPE_MOS = 0x3ec;
public static final int TYPE_ITM = 0x3ed;
public static final int TYPE_SPL = 0x3ee;
public static final int TYPE_BCS = 0x3ef;
public static final int TYPE_IDS = 0x3f0;
public static final int TYPE_CRE = 0x3f1;
public static final int TYPE_ARE = 0x3f2;
public static final int TYPE_DLG = 0x3f3;
public static final int TYPE_2DA = 0x3f4;
public static final int TYPE_GAM = 0x3f5;
public static final int TYPE_STO = 0x3f6;
public static final int TYPE_WMP = 0x3f7;
public static final int TYPE_EFF = 0x3f8;
public static final int TYPE_BS = 0x3f9;
public static final int TYPE_CHR = 0x3fa;
public static final int TYPE_VVC = 0x3fb;
public static final int TYPE_VEF = 0x3fc;
public static final int TYPE_PRO = 0x3fd;
public static final int TYPE_BIO = 0x3fe;
public static final int TYPE_WBM = 0x3ff;
public static final int TYPE_FNT = 0x400;
public static final int TYPE_GUI = 0x402;
public static final int TYPE_SQL = 0x403;
public static final int TYPE_PVRZ = 0x404;
public static final int TYPE_GLSL = 0x405;
public static final int TYPE_TOT = 0x406;
public static final int TYPE_TOH = 0x407;
public static final int TYPE_MENU = 0x408;
public static final int TYPE_LUA = 0x409;
public static final int TYPE_TTF = 0x40a;
public static final int TYPE_PNG = 0x40b;
public static final int TYPE_BAH = 0x44c;
public static final int TYPE_INI = 0x802;
public static final int TYPE_SRC = 0x803;
public static final int TYPE_MUS = 0xffe; // not in bif?
public static final int TYPE_ACM = 0xfff; // not in bif?
private static final ImageIcon ICON_TEXT = Icons.getIcon(Icons.ICON_EDIT_16);
private static final ImageIcon ICON_UNKNOWN = Icons.getIcon(Icons.ICON_HELP_16);
private static final ImageIcon ICON_SOUND = Icons.getIcon(Icons.ICON_VOLUME_16);
private static final ImageIcon ICON_MOVIE = Icons.getIcon(Icons.ICON_MOVIE_16);
private static final ImageIcon ICON_SCRIPT = Icons.getIcon(Icons.ICON_HISTORY_16);
private static final ImageIcon ICON_IMAGE = Icons.getIcon(Icons.ICON_COLOR_16);
private static final ImageIcon ICON_BUNDLE = Icons.getIcon(Icons.ICON_BUNDLE_16);
private static final String KEY_SIGNATURE = "KEY ";
private static final String KEY_VERSION = "V1 ";
private final Path keyFile; // primary key file
private final List<Path> keyList; // list of additional DLC key files
private final IntegerHashMap<String> extMap = new IntegerHashMap<String>();
private final Map<String, ImageIcon> resourceIcons = new HashMap<String, ImageIcon>();
// Map of key file path => list of associated key files
private final Map<Path, List<BIFFEntry>> biffEntries = new HashMap<>();
// Sorted map of effective BIFFResourceEntry objects
private final TreeMap<String, BIFFResourceEntry> resourceEntries = new TreeMap<>(Misc.getIgnoreCaseComparator());
public Keyfile(Path keyFile) throws IOException
{
if (keyFile == null) {
throw new NullPointerException("No keyfile specified");
}
if (!Files.isRegularFile(keyFile)) {
throw new IOException("Keyfile not found");
}
this.keyFile = keyFile;
this.keyList = new ArrayList<>();
// REMEMBER: Always use upper case letters for extensions strings
resourceIcons.clear();
resourceIcons.put("???", ICON_UNKNOWN);
extMap.put(TYPE_BMP, "BMP");
resourceIcons.put("BMP", ICON_IMAGE);
extMap.put(TYPE_MVE, "MVE");
resourceIcons.put("MVE", ICON_MOVIE);
extMap.put(TYPE_WAV, "WAV");
resourceIcons.put("WAV", ICON_SOUND);
extMap.put(TYPE_WFX, "WFX");
resourceIcons.put("WFX", ICON_STRUCT);
extMap.put(TYPE_PLT, "PLT");
resourceIcons.put("PLT", ICON_IMAGE);
extMap.put(TYPE_BAM, "BAM");
resourceIcons.put("BAM", ICON_MOVIE);
extMap.put(TYPE_WED, "WED");
resourceIcons.put("WED", ICON_STRUCT);
extMap.put(TYPE_CHU, "CHU");
resourceIcons.put("CHU", ICON_STRUCT);
extMap.put(TYPE_TIS, "TIS");
resourceIcons.put("TIS", ICON_IMAGE);
extMap.put(TYPE_MOS, "MOS");
resourceIcons.put("MOS", ICON_IMAGE);
extMap.put(TYPE_ITM, "ITM");
resourceIcons.put("ITM", ICON_STRUCT);
extMap.put(TYPE_SPL, "SPL");
resourceIcons.put("SPL", ICON_STRUCT);
extMap.put(TYPE_BCS, "BCS");
resourceIcons.put("BCS", ICON_SCRIPT);
extMap.put(TYPE_IDS, "IDS");
resourceIcons.put("IDS", ICON_TEXT);
extMap.put(TYPE_CRE, "CRE");
resourceIcons.put("CRE", ICON_STRUCT);
extMap.put(TYPE_ARE, "ARE");
resourceIcons.put("ARE", ICON_STRUCT);
extMap.put(TYPE_DLG, "DLG");
resourceIcons.put("DLG", ICON_STRUCT);
extMap.put(TYPE_2DA, "2DA");
resourceIcons.put("2DA", ICON_TEXT);
extMap.put(TYPE_GAM, "GAM");
resourceIcons.put("GAM", ICON_STRUCT);
extMap.put(TYPE_STO, "STO");
resourceIcons.put("STO", ICON_STRUCT);
extMap.put(TYPE_WMP, "WMP");
resourceIcons.put("WMP", ICON_STRUCT);
extMap.put(TYPE_EFF, "EFF");
resourceIcons.put("EFF", ICON_STRUCT);
extMap.put(TYPE_BS, "BS");
resourceIcons.put("BS", ICON_SCRIPT);
extMap.put(TYPE_CHR, "CHR");
resourceIcons.put("CHR", ICON_STRUCT);
extMap.put(TYPE_VVC, "VVC");
resourceIcons.put("VVC", ICON_STRUCT);
extMap.put(TYPE_VEF, "VEF");
resourceIcons.put("VEF", ICON_STRUCT);
extMap.put(TYPE_PRO, "PRO");
resourceIcons.put("PRO", ICON_STRUCT);
extMap.put(TYPE_BIO, "BIO");
resourceIcons.put("BIO", ICON_TEXT);
extMap.put(TYPE_WBM, "WBM");
resourceIcons.put("WBM", ICON_MOVIE);
extMap.put(TYPE_BAH, "BAH"); // ???????
extMap.put(TYPE_INI, "INI");
resourceIcons.put("INI", ICON_TEXT);
extMap.put(TYPE_SRC, "SRC");
resourceIcons.put("SRC", ICON_STRUCT);
extMap.put(TYPE_FNT, "FNT");
resourceIcons.put("FNT", ICON_IMAGE);
extMap.put(TYPE_GUI, "GUI");
resourceIcons.put("GUI", ICON_TEXT);
extMap.put(TYPE_SQL, "SQL");
resourceIcons.put("SQL", ICON_TEXT);
extMap.put(TYPE_PVRZ, "PVRZ");
resourceIcons.put("PVRZ", ICON_IMAGE);
extMap.put(TYPE_GLSL, "GLSL");
resourceIcons.put("GLSL", ICON_TEXT);
extMap.put(TYPE_TOT, "TOT");
resourceIcons.put("TOT", ICON_STRUCT);
extMap.put(TYPE_TOH, "TOH");
resourceIcons.put("TOH", ICON_STRUCT);
extMap.put(TYPE_MENU, "MENU");
resourceIcons.put("MENU", ICON_SCRIPT);
extMap.put(TYPE_LUA, "LUA");
resourceIcons.put("LUA", ICON_SCRIPT);
extMap.put(TYPE_TTF, "TTF");
resourceIcons.put("TTF", ICON_IMAGE);
extMap.put(TYPE_PNG, "PNG");
resourceIcons.put("PNG", ICON_IMAGE);
extMap.put(TYPE_MUS, "MUS");
resourceIcons.put("MUS", ICON_SOUND);
extMap.put(TYPE_ACM, "ACM");
resourceIcons.put("ACM", ICON_SOUND);
resourceIcons.put("SAV", ICON_BUNDLE);
resourceIcons.put("TXT", ICON_TEXT);
resourceIcons.put("RES", ICON_TEXT);
resourceIcons.put("BAF", ICON_SCRIPT);
}
@Override
public boolean equals(Object o)
{
if (o == this) {
return true;
} else if (o instanceof Keyfile) {
Keyfile other = (Keyfile)o;
return (keyFile.equals(other.keyFile));
}
return false;
}
@Override
public String toString()
{
return keyFile.toString();
}
/** Returns the file path to the primary key file. */
public Path getKeyfile()
{
return keyFile;
}
/** Returns all available DLC key files as unmodifiable list. */
public List<Path> getDlcKeyfiles()
{
return Collections.unmodifiableList(keyList);
}
/**
* Overrides current key file mapping with data from the specified key file.
* @param keyFile The key file containing new entries.
*/
public void addKeyfile(Path keyFile) throws IOException
{
if (keyFile == null) {
throw new NullPointerException();
}
if (!keyList.contains(keyFile)) {
keyList.add(keyFile);
}
}
public void populateResourceTree(ResourceTreeModel treeModel) throws Exception
{
if (treeModel != null) {
init();
resourceEntries.values().forEach((entry) -> treeModel.addResourceEntry(entry, entry.getExtension(), true));
cacheBIFFs();
}
}
/** Returns the resource extension string of specified type. */
public String getExtension(int type)
{
return extMap.get(type);
}
/** Attempts to determine the resource type of the specified extension. */
public int getExtensionType(String extension)
{
if (extension != null) {
if (extension.length() > 0 && extension.charAt(0) == '.') {
extension = extension.substring(1);
}
extension = extension.toUpperCase(Locale.ENGLISH);
int[] keys = extMap.keys();
for (final int type: keys) {
if (extMap.get(type).equals(extension)) {
return type;
}
}
}
return -1;
}
public ImageIcon getIcon(String extension)
{
ImageIcon icon = resourceIcons.get(extension);
if (icon == null) {
icon = resourceIcons.get("???");
}
return icon;
}
public void closeBIFFFiles()
{
AbstractBIFFReader.resetCache();
}
public void addBIFFEntry(BIFFEntry entry)
{
if (entry != null) {
List<BIFFEntry> biffList = getBIFFList(getKeyfile(), false);
if(biffList != null) {
biffList.add(entry);
entry.setIndex(biffList.size() - 1);
}
}
}
// public boolean cleanUp()
// {
// try {
// closeBIFFFiles();
// List<BIFFEntry> biffList = biffEntries.get(getKeyfile());
// Set<BIFFEntry> toRemove = new HashSet<BIFFEntry>(biffList);
// // Determine BIFFs with no files in them
// List<BIFFResourceEntry> resourceEntries = loadResourceEntries(getKeyfile());
// resourceEntries.forEach((entry) -> toRemove.remove(entry.getBIFFEntry()));
//
// // Delete these BIFFs
// toRemove.forEach((entry) -> {
// Path file = entry.getPath();
// System.out.println("Deleting " + file);
// if (file != null) {
// try {
// Files.deleteIfExists(file);
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// });
//
// // Determine non-existant BIFFs
// biffList.forEach((entry) -> {
// if (entry.getPath() == null) {
// toRemove.add(entry);
// }
// });
// if (toRemove.isEmpty()) {
// return false;
// }
//
// // Remove bogus BIFFs from keyfile
// toRemove.forEach((entry) -> removeBIFFEntry(getKeyfile(), entry));
//
// return true;
// } catch (IOException e) {
// e.printStackTrace();
// }
//
// return false;
// }
public BIFFEntry[] getBIFFEntriesSorted()
{
List<BIFFEntry> biffList = new ArrayList<>();
for (final List<BIFFEntry> list: biffEntries.values()) {
biffList.addAll(list);
}
Collections.sort(biffList);
return biffList.toArray(new BIFFEntry[biffList.size()]);
}
public BIFFEntry getBIFFEntry(Path keyFile, int index)
{
List<BIFFEntry> biffs = getBIFFList(keyFile, false);
if (biffs != null) {
return biffs.get(index);
}
return null;
}
public AbstractBIFFReader getBIFFFile(BIFFEntry entry) throws Exception
{
if (entry == null) {
return null;
} else if (entry.getPath() == null) {
throw new IOException(entry + " not found");
} else {
return AbstractBIFFReader.open(entry.getPath());
}
}
public void write() throws IOException
{
List<BIFFEntry> biffs = getBIFFList(getKeyfile(), false);
if (biffs == null) {
throw new IOException("Error loading BIFF entry table");
}
try (OutputStream os = StreamUtils.getOutputStream(getKeyfile())) {
int bifoff = 0x18;
int offset = bifoff + 0x0c*biffs.size();
for (final BIFFEntry biff: biffs) {
offset += biff.updateOffset(offset);
}
int resoff = offset;
List<BIFFResourceEntry> entries = ResourceFactory.getResources().getBIFFResourceEntries(getKeyfile());
StreamUtils.writeString(os, KEY_SIGNATURE, 4);
StreamUtils.writeString(os, KEY_VERSION, 4);
StreamUtils.writeInt(os, biffs.size());
StreamUtils.writeInt(os, entries.size());
StreamUtils.writeInt(os, bifoff);
StreamUtils.writeInt(os, resoff);
for (final BIFFEntry biff: biffs) {
biff.write(os);
}
for (final BIFFEntry biff: biffs) {
biff.writeString(os);
}
for (final BIFFResourceEntry entry: entries) {
entry.write(os);
}
}
}
// caches all BIFF files referenced in the current KEY file
private void cacheBIFFs()
{
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run()
{
biffEntries.values().forEach((biffList) -> {
biffList.forEach((entry) -> {
if (entry != null) {
Path biffPath = entry.getPath();
if (biffPath != null && Files.isRegularFile(biffPath)) {
try {
AbstractBIFFReader.open(biffPath);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
});
}
});
}
// Creates a list of ResourceEntry objects from the specified key file
// private List<BIFFResourceEntry> loadResourceEntries(Path keyFile) throws IOException
// {
// if (keyFile == null) {
// throw new NullPointerException();
// }
// if (!Files.isRegularFile(keyFile)) {
// throw new IOException("Key file not found: " + keyFile);
// }
//
// try (SeekableByteChannel ch = Files.newByteChannel(keyFile, StandardOpenOption.READ)) {
// ByteBuffer buffer = StreamUtils.getByteBuffer((int)ch.size());
// if (ch.read(buffer) < ch.size()) {
// throw new IOException("Error loading key file");
// }
//
// String sig = StreamUtils.readString(buffer, 0, 4);
// String ver = StreamUtils.readString(buffer, 4, 4);
// if (!sig.equals(KEY_SIGNATURE) || !ver.equals(KEY_VERSION)) {
// throw new IOException("Unsupported key file: " + keyFile.toString());
// }
//
// int numRes = buffer.getInt(0x0c);
// int ofsRes = buffer.getInt(0x14);
//
// List<BIFFResourceEntry> retVal = new ArrayList<>(numRes);
// for (int i = 0; i < numRes; i++) {
// retVal.add(new BIFFResourceEntry(keyFile, buffer, ofsRes + i*14, 8));
// }
// return retVal;
// }
// }
// Creates or updates cached biff maps and entry tables
private void init() throws IOException
{
if (getKeyfile() == null) {
throw new NullPointerException();
}
if (!Files.isRegularFile(getKeyfile())) {
throw new IOException("Key file not found: " + getKeyfile());
}
for (final Path file: keyList) {
if (file != null && !Files.isRegularFile(file)) {
throw new IOException("Key file not found: " + file);
}
}
closeBIFFFiles();
resourceEntries.clear();
biffEntries.clear();
List<Path> keyFiles = new ArrayList<>(1 + keyList.size());
keyFiles.add(getKeyfile());
keyFiles.addAll(keyList);
for (final Path file: keyFiles) {
try (SeekableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.READ)) {
ByteBuffer buffer = StreamUtils.getByteBuffer((int)ch.size());
if (ch.read(buffer) < ch.size()) {
throw new IOException("Error loading key file");
}
String sig = StreamUtils.readString(buffer, 0, 4);
String ver = StreamUtils.readString(buffer, 4, 4);
if (!sig.equals(KEY_SIGNATURE) || !ver.equals(KEY_VERSION)) {
throw new IOException("Unsupported key file: " + file.toString());
}
int numBif = buffer.getInt(0x08);
int numRes = buffer.getInt(0x0c);
int ofsBif = buffer.getInt(0x10);
int ofsRes = buffer.getInt(0x14);
List<BIFFEntry> biffList = getBIFFList(file, true);
if (biffList == null) {
biffList = new ArrayList<>(numBif);
} else {
// discard old entries
biffList.clear();
}
// processing BIFF entries
for (int i = 0, ofs = ofsBif; i < numBif; i++, ofs += 12) {
biffList.add(new BIFFEntry(file, i, buffer, ofs));
}
biffEntries.put(file, biffList);
// processing resource entries
for (int i = 0, ofs = ofsRes; i < numRes; i++, ofs += 14) {
addResourceEntry(new BIFFResourceEntry(file, buffer, ofs, 8));
}
}
}
}
// Returns the list of BIFFEntry objects for the specified key file, optionally removes it
private List<BIFFEntry> getBIFFList(Path keyFile, boolean remove)
{
if (keyFile != null) {
if (remove) {
return biffEntries.remove(keyFile);
} else {
return biffEntries.get(keyFile);
}
}
return null;
}
// Adds the specified resource entry to the list, overwrites existing entries of same name.
private BIFFResourceEntry addResourceEntry(BIFFResourceEntry entry)
{
BIFFResourceEntry retVal = null;
if (entry != null) {
String key = entry.toString();
retVal = resourceEntries.get(key);
resourceEntries.put(key, entry);
}
return retVal;
}
// Removes the specified BIFF entry and associated resource entries from cache and resource tree
// private void removeBIFFEntry(Path keyFile, BIFFEntry entry)
// {
// System.out.println("Removing " + entry);
// List<BIFFEntry> biffList = biffEntries.get(keyFile);
// int index = biffList.indexOf(entry);
//
// // Remove bogus BIFFResourceEntries
// ResourceTreeModel resources = ResourceFactory.getResources();
// resources.getBIFFResourceEntries(keyFile).forEach((resourceEntry) -> {
// if (resourceEntry.getBIFFEntry() == entry) {
// resources.removeResourceEntry(resourceEntry);
// resourceEntries.remove(resourceEntry);
// } else {
// resourceEntry.adjustSourceIndex(index); // Update relevant BIFFResourceEntries
// }
// });
//
// // Remove BIFFEntry
// biffList.remove(entry);
//
// // Update relevant BIFFEntries
// for (int i = index; i < biffList.size(); i++) {
// BIFFEntry e = biffList.get(i);
// e.setIndex(i);
// }
// }
}