// 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;
import java.awt.Component;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileSystemView;
import org.infinity.NearInfinity;
import org.infinity.datatype.PriTypeBitmap;
import org.infinity.datatype.SecTypeBitmap;
import org.infinity.datatype.Song2daBitmap;
import org.infinity.datatype.Summon2daBitmap;
import org.infinity.gui.BrowserMenuBar;
import org.infinity.gui.ChildFrame;
import org.infinity.gui.IdsBrowser;
import org.infinity.resource.are.AreResource;
import org.infinity.resource.bcs.BafResource;
import org.infinity.resource.bcs.BcsResource;
import org.infinity.resource.bcs.Compiler;
import org.infinity.resource.chu.ChuResource;
import org.infinity.resource.cre.CreResource;
import org.infinity.resource.dlg.DlgResource;
import org.infinity.resource.gam.GamResource;
import org.infinity.resource.graphics.BamResource;
import org.infinity.resource.graphics.GraphicsResource;
import org.infinity.resource.graphics.MosResource;
import org.infinity.resource.graphics.PltResource;
import org.infinity.resource.graphics.PvrzResource;
import org.infinity.resource.graphics.TisResource;
import org.infinity.resource.itm.ItmResource;
import org.infinity.resource.key.BIFFResourceEntry;
import org.infinity.resource.key.FileResourceEntry;
import org.infinity.resource.key.Keyfile;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.resource.key.ResourceTreeFolder;
import org.infinity.resource.key.ResourceTreeModel;
import org.infinity.resource.mus.MusResource;
import org.infinity.resource.other.EffResource;
import org.infinity.resource.other.FntResource;
import org.infinity.resource.other.TtfResource;
import org.infinity.resource.other.UnknownResource;
import org.infinity.resource.other.VvcResource;
import org.infinity.resource.other.WfxResource;
import org.infinity.resource.pro.ProResource;
import org.infinity.resource.sav.SavResource;
import org.infinity.resource.sound.SoundResource;
import org.infinity.resource.spl.SplResource;
import org.infinity.resource.src.SrcResource;
import org.infinity.resource.sto.StoResource;
import org.infinity.resource.text.PlainTextResource;
import org.infinity.resource.to.TohResource;
import org.infinity.resource.to.TotResource;
import org.infinity.resource.var.VarResource;
import org.infinity.resource.vef.VefResource;
import org.infinity.resource.video.MveResource;
import org.infinity.resource.video.WbmResource;
import org.infinity.resource.wed.WedResource;
import org.infinity.resource.wmp.WmpResource;
import org.infinity.util.Decryptor;
import org.infinity.util.DynamicArray;
import org.infinity.util.IdsMapCache;
import org.infinity.util.Misc;
import org.infinity.util.StringResource;
import org.infinity.util.io.FileManager;
import org.infinity.util.io.StreamUtils;
/**
* Handles game-specific resource access.
*/
public final class ResourceFactory
{
private static ResourceFactory instance;
private JFileChooser fc;
private Keyfile keyfile;
private ResourceTreeModel treeModel;
public static Keyfile getKeyfile()
{
if (getInstance() != null) {
return getInstance().keyfile;
} else {
return null;
}
}
public static Resource getResource(ResourceEntry entry)
{
return getResource(entry, null);
}
public static Resource getResource(ResourceEntry entry, String forcedExtension)
{
Resource res = null;
try {
String ext = (forcedExtension != null) ? forcedExtension : entry.getExtension();
if (ext.equalsIgnoreCase("BAM")) {
res = new BamResource(entry);
} else if (ext.equalsIgnoreCase("TIS")) {
res = new TisResource(entry);
} else if (ext.equalsIgnoreCase("BMP") || ext.equalsIgnoreCase("PNG")) {
res = new GraphicsResource(entry);
} else if (ext.equalsIgnoreCase("MOS")) {
res = new MosResource(entry);
} else if (ext.equalsIgnoreCase("WAV") || ext.equalsIgnoreCase("ACM")) {
res = new SoundResource(entry);
} else if (ext.equalsIgnoreCase("MUS")) {
res = new MusResource(entry);
} else if (ext.equalsIgnoreCase("IDS") || ext.equalsIgnoreCase("2DA") ||
ext.equalsIgnoreCase("BIO") || ext.equalsIgnoreCase("RES") ||
ext.equalsIgnoreCase("INI") || ext.equalsIgnoreCase("TXT") ||
(ext.equalsIgnoreCase("SRC") && Profile.getEngine() == Profile.Engine.IWD2) ||
(Profile.isEnhancedEdition() && (ext.equalsIgnoreCase("SQL") ||
ext.equalsIgnoreCase("GUI") ||
ext.equalsIgnoreCase("LUA") ||
ext.equalsIgnoreCase("MENU") ||
ext.equalsIgnoreCase("GLSL")))) {
res = new PlainTextResource(entry);
} else if (ext.equalsIgnoreCase("MVE")) {
res = new MveResource(entry);
} else if (ext.equalsIgnoreCase("WBM")) {
res = new WbmResource(entry);
} else if (ext.equalsIgnoreCase("PLT") && ext.equals(forcedExtension)) {
res = new PltResource(entry);
} else if (ext.equalsIgnoreCase("BCS") || ext.equalsIgnoreCase("BS")) {
res = new BcsResource(entry);
} else if (ext.equalsIgnoreCase("ITM")) {
res = new ItmResource(entry);
} else if (ext.equalsIgnoreCase("EFF")) {
res = new EffResource(entry);
} else if (ext.equalsIgnoreCase("VEF")) {
res = new VefResource(entry);
} else if (ext.equalsIgnoreCase("VVC")) {
res = new VvcResource(entry);
} else if (ext.equalsIgnoreCase("SRC")) {
res = new SrcResource(entry);
} else if (ext.equalsIgnoreCase("DLG")) {
res = new DlgResource(entry);
} else if (ext.equalsIgnoreCase("SPL")) {
res = new SplResource(entry);
} else if (ext.equalsIgnoreCase("STO")) {
res = new StoResource(entry);
} else if (ext.equalsIgnoreCase("WMP")) {
res = new WmpResource(entry);
} else if (ext.equalsIgnoreCase("CHU")) {
res = new ChuResource(entry);
} else if (ext.equalsIgnoreCase("CRE") || ext.equalsIgnoreCase("CHR")) {
res = new CreResource(entry);
} else if (ext.equalsIgnoreCase("ARE")) {
res = new AreResource(entry);
} else if (ext.equalsIgnoreCase("WFX")) {
res = new WfxResource(entry);
} else if (ext.equalsIgnoreCase("PRO")) {
res = new ProResource(entry);
} else if (ext.equalsIgnoreCase("WED")) {
res = new WedResource(entry);
} else if (ext.equalsIgnoreCase("GAM")) {
res = new GamResource(entry);
} else if (ext.equalsIgnoreCase("SAV")) {
res = new SavResource(entry);
} else if (ext.equalsIgnoreCase("VAR")) {
res = new VarResource(entry);
} else if (ext.equalsIgnoreCase("BAF")) {
res = new BafResource(entry);
} else if (ext.equalsIgnoreCase("TOH")) {
res = new TohResource(entry);
} else if (ext.equalsIgnoreCase("TOT")) {
res = new TotResource(entry);
} else if (ext.equalsIgnoreCase("PVRZ") && Profile.isEnhancedEdition()) {
res = new PvrzResource(entry);
} else if (ext.equalsIgnoreCase("FNT") && Profile.isEnhancedEdition()) {
res = new FntResource(entry);
} else if (ext.equalsIgnoreCase("TTF") && Profile.isEnhancedEdition()) {
res = new TtfResource(entry);
} else {
res = detectResource(entry);
if (res == null) {
res = new UnknownResource(entry);
}
}
} catch (Exception e) {
if (NearInfinity.getInstance() != null && !BrowserMenuBar.getInstance().ignoreReadErrors()) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(),
"Error reading " + entry + '\n' + e.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
} else {
final String msg = String.format("Error reading %1$s @ %2$s - %3$s",
entry, entry.getActualPath(), e);
NearInfinity.getInstance().getStatusBar().setMessage(msg);
}
System.err.println("Error reading " + entry);
e.printStackTrace();
}
return res;
}
/**
* Attempts to detect the resource type from the data itself
* and returns the respective resource class instance, or {@code null} on failure.
*/
public static Resource detectResource(ResourceEntry entry)
{
Resource res = null;
if (entry != null) {
try {
int[] info = entry.getResourceInfo();
if (info.length == 2) {
res = getResource(entry, "TIS");
} else if (info.length == 1) {
if (info[0] > 4) {
byte[] data = new byte[Math.min(info[0], 24)];
try (InputStream is = entry.getResourceDataAsStream()) {
StreamUtils.readBytes(is, data);
}
String sig = DynamicArray.getString(data, 0, 4);
if ("2DA ".equalsIgnoreCase(sig)) {
res = getResource(entry, "2DA");
} else if ("ARE ".equals(sig)) {
res = getResource(entry, "ARE");
} else if ("BAM ".equals(sig) || "BAMC".equals(sig)) {
res = getResource(entry, "BAM");
} else if ("CHR ".equals(sig)) {
res = getResource(entry, "CHR");
} else if ("CHUI".equals(sig)) {
res = getResource(entry, "CHU");
} else if ("CRE ".equals(sig)) {
res = getResource(entry, "CRE");
} else if ("DLG ".equals(sig)) {
res = getResource(entry, "DLG");
} else if ("EFF ".equals(sig)) {
res = getResource(entry, "EFF");
} else if ("GAME".equals(sig)) {
res = getResource(entry, "GAM");
} else if ("IDS ".equalsIgnoreCase(sig)) {
res = getResource(entry, "IDS");
} else if ("ITM ".equals(sig)) {
res = getResource(entry, "ITM");
} else if ("MOS ".equals(sig) || "MOSC".equals(sig)) {
res = getResource(entry, "MOS");
} else if ("PLT ".equals(sig)) {
res = getResource(entry, "PLT");
} else if ("PRO ".equals(sig)) {
res = getResource(entry, "PRO");
} else if ("SAV ".equals(sig)) {
res = getResource(entry, "SAV");
} else if ("SPL ".equals(sig)) {
res = getResource(entry, "SPL");
} else if ("STOR".equals(sig)) {
res = getResource(entry, "STO");
} else if ("TIS ".equals(sig)) {
res = getResource(entry, "TIS");
} else if ("VEF ".equals(sig)) {
res = getResource(entry, "VEF");
} else if ("VVC ".equals(sig)) {
res = getResource(entry, "VVC");
} else if ("WAVC".equals(sig) || "RIFF".equals(sig) || "OggS".equals(sig)) {
res = getResource(entry, "WAV");
} else if ("WED ".equals(sig)) {
res = getResource(entry, "WED");
} else if ("WFX ".equals(sig)) {
res = getResource(entry, "WFX");
} else if ("WMAP".equals(sig)) {
res = getResource(entry, "WMP");
} else {
if ((Arrays.equals(new byte[]{0x53, 0x43, 0x0a}, Arrays.copyOfRange(data, 0, 3)) || // == "SC\n"
Arrays.equals(new byte[]{0x53, 0x43, 0x0d, 0x0a}, Arrays.copyOfRange(data, 0, 4)))) { // == "SC\r\n"
res = getResource(entry, "BCS");
} else if (data.length > 6 && "BM".equals(new String(data, 0, 2)) &&
DynamicArray.getInt(data, 2) == info[0]) {
res = getResource(entry, "BMP");
} else if (data.length > 18 && "Interplay MVE File".equals(new String(data, 0, 18))) {
res = getResource(entry, "MVE");
} else if (Arrays.equals(new byte[]{(byte)0x1a, (byte)0x45, (byte)0xdf, (byte)0xa3},
Arrays.copyOfRange(data, 0, 4))) {
res = getResource(entry, "WBM");
} else if (data.length > 6 && data[3] == 0 && data[4] == 0x78) { // just guessing...
res = getResource(entry, "PVRZ");
} else if (data.length > 4 && data[0] == 0x89 &&
data[1] == 0x50 && data[2] == 0x4e && data[3] == 0x47) {
res = getResource(entry, "PNG");
} else if (DynamicArray.getInt(data, 0) == 0x00000100) { // wild guess...
res = getResource(entry, "TTF");
}
}
}
} else {
throw new Exception(entry.getResourceName() + ": Unable to determine resource type");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return res;
}
public static void exportResource(ResourceEntry entry, Component parent)
{
if (getInstance() != null) {
try {
getInstance().exportResourceInternal(entry, parent, null);
} catch (Exception e) {
e.printStackTrace();
JOptionPane.showMessageDialog(parent, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public static void exportResource(ResourceEntry entry, ByteBuffer buffer, String filename, Component parent)
{
if (getInstance() != null) {
try {
getInstance().exportResourceInternal(entry, buffer, filename, parent, null);
} catch (Exception e) {
e.printStackTrace();
JOptionPane.showMessageDialog(parent, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
/** Exports "entry" to "output" without any user interaction. */
public static void exportResource(ResourceEntry entry, Path output) throws Exception
{
if (getInstance() != null && output != null) {
getInstance().exportResourceInternal(entry, null, output);
}
}
/**
* Returns whether the specified resource exists.
* @param resourceName The resource filename.
* @return {@code true} if the resource exists in BIFF archives or override folders,
* {@code false} otherwise.
*/
public static boolean resourceExists(String resourceName)
{
return (getResourceEntry(resourceName, false) != null);
}
/**
* Returns whether the specified resource exists.
* @param resourceName The resource filename.
* @param searchExtraDirs If {@code true}, all supported override folders will be searched.
* If {@code false}, only the default 'override' folders will be searched.
* @return {@code true} if the resource exists in BIFF archives or override folders,
* {@code false} otherwise.
*/
public static boolean resourceExists(String resourceName, boolean searchExtraDirs)
{
return (getResourceEntry(resourceName, searchExtraDirs) != null);
}
/**
* Returns whether the specified resource exists.
* @param resourceName The resource filename.
* @param searchExtraDirs If {@code true}, all supported override folders will be searched.
* If {@code false}, only the default 'override' folders will be searched.
* @param extraDirs A list of File entries pointing to additional folders to search, not
* covered by the default override folder list (e.g. "install:/music").
* @return {@code true} if the resource exists in BIFF archives or override folders,
* {@code false} otherwise.
*/
public static boolean resourceExists(String resourceName, boolean searchExtraDirs, List<Path> extraDirs)
{
return (getResourceEntry(resourceName, searchExtraDirs, extraDirs) != null);
}
/**
* Returns a ResourceEntry instance of the given resource name.
* @param resourceName The resource filename.
* @return A ResourceEntry instance of the given resource filename, or {@code null} if not
* available.
*/
public static ResourceEntry getResourceEntry(String resourceName)
{
return getResourceEntry(resourceName, false, null);
}
/**
* Returns a ResourceEntry instance of the given resource name.
* @param resourceName The resource filename.
* @param searchExtraDirs If {@code true}, all supported override folders will be searched.
* If {@code false}, only the default 'override' folders will be searched.
* @return A ResourceEntry instance of the given resource filename, or {@code null} if not
* available.
*/
public static ResourceEntry getResourceEntry(String resourceName, boolean searchExtraDirs)
{
return getResourceEntry(resourceName, searchExtraDirs, null);
}
/**
* Returns a ResourceEntry instance of the given resource name.
* @param resourceName The resource filename.
* @param searchExtraDirs If {@code true}, all supported override folders will be searched.
* If {@code false}, only the default 'override' folders will be searched.
* @param extraDirs A list of File entries pointing to additional folders to search, not
* covered by the default override folder list (e.g. "install:/music").
* @return A ResourceEntry instance of the given resource filename, or {@code null} if not
* available.
*/
public static ResourceEntry getResourceEntry(String resourceName, boolean searchExtraDirs, List<Path> extraDirs)
{
if (getInstance() != null) {
ResourceEntry entry = getInstance().treeModel.getResourceEntry(resourceName);
// checking default override folder list
if (searchExtraDirs && (entry == null)) {
List<Path> extraFolders = Profile.getOverrideFolders(false);
if (extraFolders != null) {
Path file = FileManager.query(extraFolders, resourceName);
if (file != null && Files.isRegularFile(file)) {
entry = new FileResourceEntry(file);
}
}
}
// checking custom folder list
if (extraDirs != null && (entry == null)) {
Path file = FileManager.query(extraDirs, resourceName);
if (file != null && Files.isRegularFile(file)) {
entry = new FileResourceEntry(file);
}
}
return entry;
} else {
return null;
}
}
public static ResourceTreeModel getResources()
{
if (getInstance() != null) {
return getInstance().treeModel;
} else {
return null;
}
}
public static List<ResourceEntry> getResources(String type)
{
return getResources(type, Profile.getProperty(Profile.Key.GET_GAME_EXTRA_FOLDERS));
}
public static List<ResourceEntry> getResources(String type, List<Path> extraDirs)
{
if (getInstance() != null) {
return getInstance().getResourcesInternal(type, extraDirs);
} else {
return null;
}
}
public static void loadResources() throws Exception
{
if (getInstance() != null) {
getInstance().loadResourcesInternal();
}
}
public static void saveCopyOfResource(ResourceEntry entry)
{
if (getInstance() != null) {
getInstance().saveCopyOfResourceInternal(entry);
}
}
public static boolean saveResource(Resource resource, Component parent)
{
if (getInstance() != null) {
return getInstance().saveResourceInternal(resource, parent);
} else {
return false;
}
}
/**
* Returns a list of available game language directories for the current game in Enhanced Edition games.
* Returns an empty list otherwise.
*/
public static List<Path> getAvailableGameLanguages()
{
List<Path> list = new ArrayList<>();
if (Profile.isEnhancedEdition()) {
Path langPath = Profile.getProperty(Profile.Key.GET_GAME_LANG_FOLDER_BASE);
if (langPath != null && Files.isDirectory(langPath)) {
try (DirectoryStream<Path> dstream = Files.newDirectoryStream(langPath,
(Path entry) -> {
return Files.isDirectory(entry) &&
entry.getFileName().toString().matches("[a-z]{2}_[A-Z]{2}") &&
Files.isRegularFile(FileManager.query(entry, Profile.getProperty(Profile.Key.GET_GLOBAL_DIALOG_NAME)));
})) {
dstream.forEach((path) -> list.add(path));
} catch (IOException e) {
e.printStackTrace();
}
}
}
return list;
}
/** Return the game language specified in the given baldur.ini if found. Returns {@code en_US} otherwise. */
public static String autodetectGameLanguage(Path iniFile)
{
final String langDefault = "en_US"; // using default language, if no language entry found
if (Profile.isEnhancedEdition() && iniFile != null && Files.isRegularFile(iniFile)) {
// Attempt to autodetect game language
try (BufferedReader br = Files.newBufferedReader(iniFile, Misc.CHARSET_ASCII)) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains("'Language'")) {
String[] entries = line.split(",");
if (entries.length == 3) {
// Note: replace operation is compatible with both baldur.ini and baldur.lua
String lang = entries[2].replaceFirst("^[^']*'", "");
lang = lang.replaceFirst("'.*$", "");
if (lang.matches("[A-Za-z]{2}_[A-Za-z]{2}")) {
Path path = FileManager.query(Profile.getGameRoot(), "lang", lang);
if (path != null && Files.isDirectory(path)) {
try {
// try to fetch the actual path name to ensure correct case
return path.toRealPath().getFileName().toString();
} catch (Exception e) {
return lang;
}
}
}
}
}
}
} catch (IOException e) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error parsing " + iniFile.getFileName() +
". Using language defaults.", "Error", JOptionPane.ERROR_MESSAGE);
}
}
return langDefault;
}
/** Attempts to find the home folder of an Enhanced Edition game. */
static Path getHomeRoot()
{
if (Profile.hasProperty(Profile.Key.GET_GAME_HOME_FOLDER_NAME)) {
final Path EE_DOC_ROOT = FileSystemView.getFileSystemView().getDefaultDirectory().toPath();
final String EE_DIR = Profile.getProperty(Profile.Key.GET_GAME_HOME_FOLDER_NAME);
Path userPath = FileManager.query(EE_DOC_ROOT, EE_DIR);
if (userPath != null && Files.isDirectory(userPath)) {
return userPath;
} else {
// fallback solution
String userPrefix = System.getProperty("user.home");
userPath = null;
String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
if (osName.contains("windows")) {
try {
Process p = Runtime.getRuntime().exec("reg query \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\" /v personal");
p.waitFor();
InputStream in = p.getInputStream();
byte[] b = new byte[in.available()];
in.read(b);
in.close();
String[] splitted = new String(b).split("\\s\\s+");
userPrefix = splitted[splitted.length-1];
userPath = FileManager.resolve(userPrefix, EE_DIR);
} catch (Throwable t) {
return null;
}
} else if (osName.contains("mac") || osName.contains("darwin")) {
userPath = FileManager.resolve(FileManager.resolve(userPrefix, "Documents", EE_DIR));
} else if (osName.contains("nix") || osName.contains("nux") || osName.contains("bsd")) {
userPath = FileManager.resolve(FileManager.resolve(userPrefix, ".local", "share", EE_DIR));
}
if (userPath != null && Files.isDirectory(userPath)) {
return userPath;
}
}
}
return null;
}
/** Attempts to find folders containing BIFF archives. */
static List<Path> getBIFFDirs()
{
List<Path> dirList = new ArrayList<>();
if (Profile.isEnhancedEdition()) {
// adding supported base biff folders
Path langRoot = Profile.getLanguageRoot();
if (langRoot != null) {
dirList.add(langRoot);
}
List<Path> dlcList = Profile.getProperty(Profile.Key.GET_GAME_DLC_FOLDERS_AVAILABLE);
if (dlcList != null) {
dlcList.forEach((path) -> dirList.add(path));
}
dirList.add(Profile.getGameRoot());
} else {
// fetching the CD folders in a game installation
Path iniFile = Profile.getProperty(Profile.Key.GET_GAME_INI_FILE);
List<Path> rootFolders = Profile.getRootFolders();
if (iniFile != null && Files.isRegularFile(iniFile)) {
try (BufferedReader br = Files.newBufferedReader(iniFile)) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains(":=")) {
String[] items = line.split(":=");
if (items.length > 1) {
int p = items[1].indexOf(';');
if (p >= 0) {
line = items[1].substring(0, p).trim();
} else {
line = items[1].trim();
}
if (line.endsWith(":")) {
line = line.replace(':', '/');
}
// Try to handle Mac relative paths
Path path;
if (line.charAt(0) == '/') {
path = FileManager.query(rootFolders, line);
} else {
path = FileManager.resolve(line);
}
if (Files.isDirectory(path)) {
dirList.add(path);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
dirList.clear();
}
}
if (dirList.isEmpty()) {
// Don't panic if an .ini-file cannot be found or contains errors
Path path;
for (int i = 1; i < 7; i++) {
path = FileManager.query(rootFolders, "CD" + i);
if (Files.isDirectory(path)) {
dirList.add(path);
}
}
// used in certain games
path = FileManager.query(rootFolders, "CDALL");
if (Files.isDirectory(path)) {
dirList.add(path);
}
}
}
return dirList;
}
// Returns the currently used language of an Enhanced Edition game.
static String fetchGameLanguage(Path iniFile)
{
final String langDefault = "en_US"; // using default language, if no language entry found
if (Profile.isEnhancedEdition() && iniFile != null && Files.isRegularFile(iniFile)) {
String lang = BrowserMenuBar.getInstance().getSelectedGameLanguage();
if (lang == null || lang.isEmpty()) {
return autodetectGameLanguage(iniFile);
} else {
// Using user-defined language
if (lang.matches("[A-Za-z]{2}_[A-Za-z]{2}")) {
Path path = FileManager.query(Profile.getGameRoot(), "lang", lang);
if (path != null && Files.isDirectory(path)) {
String retVal;
try {
// try to fetch the actual path name to ensure correct case
retVal = path.toRealPath().getFileName().toString();
} catch (Exception e) {
retVal = lang;
}
return retVal;
}
}
}
}
// falling back to default language
return langDefault;
}
/** Used internally by the Profile class to open and initialize a new game. */
static void openGame(Path keyFile)
{
closeGame();
new ResourceFactory(keyFile);
}
/** Closes the current game configuration. */
private static void closeGame()
{
if (instance != null) {
instance.close();
instance = null;
}
}
private static ResourceFactory getInstance()
{
return instance;
}
private ResourceFactory(Path keyFile)
{
instance = this;
try {
// initializing primary key file
this.keyfile = new Keyfile(keyFile);
// adding DLC key files if available
List<Path> keyList = Profile.getProperty(Profile.Key.GET_GAME_DLC_KEYS_AVAILABLE);
if (keyList != null) {
for (final Path key: keyList) {
this.keyfile.addKeyfile(key);
}
}
loadResourcesInternal();
} catch (Exception e) {
JOptionPane.showMessageDialog(null, "No Infinity Engine game found", "Error",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
// Cleans up resources
private void close()
{
// nothing to do yet...
}
private void exportResourceInternal(ResourceEntry entry, Component parent, Path output) throws Exception
{
try {
ByteBuffer buffer = entry.getResourceBuffer();
final String ext = entry.getExtension();
if (ext.equalsIgnoreCase("IDS") || ext.equalsIgnoreCase("2DA") ||
ext.equalsIgnoreCase("BIO") || ext.equalsIgnoreCase("RES") ||
ext.equalsIgnoreCase("INI") || ext.equalsIgnoreCase("SET") ||
ext.equalsIgnoreCase("TXT") ||
(Profile.getEngine() == Profile.Engine.IWD2 && ext.equalsIgnoreCase("SRC")) ||
(Profile.isEnhancedEdition() && (ext.equalsIgnoreCase("GUI") ||
ext.equalsIgnoreCase("SQL") ||
ext.equalsIgnoreCase("GLSL")))) {
if (buffer.getShort(0) == -1) {
exportResourceInternal(entry, Decryptor.decrypt(buffer, 2), entry.toString(), parent, output);
} else {
buffer.position(0);
exportResourceInternal(entry, buffer, entry.toString(), parent, output);
}
} else {
exportResourceInternal(entry, buffer, entry.toString(), parent, output);
}
} catch (Exception e) {
throw new Exception("Can't read " + entry);
}
}
private void exportResourceInternal(ResourceEntry entry, ByteBuffer buffer, String fileName,
Component parent, Path output) throws Exception
{
// ask for output file path if needed
boolean interactive = (output == null);
if (interactive) {
if (fc == null) {
fc = new JFileChooser(Profile.getGameRoot().toFile());
fc.setDialogTitle("Export resource");
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
}
fc.setSelectedFile(new File(fc.getCurrentDirectory(), fileName));
if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
output = fc.getSelectedFile().toPath();
if (Files.exists(output)) {
final String options[] = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(parent, output + " exists. Overwrite?", "Export resource",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
null, options, options[0]) != 0) {
return;
}
}
}
}
// exporting resource
if (output != null) {
try {
try (OutputStream os = StreamUtils.getOutputStream(output, true)) {
StreamUtils.writeBytes(os, buffer);
}
if (interactive) {
JOptionPane.showMessageDialog(parent, "File exported to " + output, "Export complete",
JOptionPane.INFORMATION_MESSAGE);
}
} catch (IOException e) {
throw new Exception("Error while exporting " + entry);
}
}
}
private void loadResourcesInternal() throws Exception
{
treeModel = new ResourceTreeModel();
// Get resources from keyfile
NearInfinity.advanceProgress("Loading BIFF resources...");
keyfile.populateResourceTree(treeModel);
StringResource.init(Profile.getProperty(Profile.Key.GET_GAME_DIALOG_FILE));
// Add resources from extra folders
NearInfinity.advanceProgress("Loading extra resources...");
List<Path> extraPaths = Profile.getProperty(Profile.Key.GET_GAME_EXTRA_FOLDERS);
extraPaths.forEach((path) -> {
if (Files.isDirectory(path)) {
treeModel.addDirectory((ResourceTreeFolder)treeModel.getRoot(), path, false);
}
});
NearInfinity.advanceProgress("Loading override resources...");
final boolean overrideInOverride = (BrowserMenuBar.getInstance() != null &&
BrowserMenuBar.getInstance().getOverrideMode() == BrowserMenuBar.OVERRIDE_IN_OVERRIDE);
String overrideFolder = Profile.getOverrideFolderName();
List<Path> overridePaths = Profile.getOverrideFolders(false);
for (final Path overridePath: overridePaths) {
if (Files.isDirectory(overridePath)) {
try (DirectoryStream<Path> dstream = Files.newDirectoryStream(overridePath)) {
dstream.forEach((path) -> {
if (Files.isRegularFile(path)) {
ResourceEntry entry = getResourceEntry(path.getFileName().toString());
if (entry == null) {
FileResourceEntry fileEntry = new FileResourceEntry(path, true);
treeModel.addResourceEntry(fileEntry, fileEntry.getTreeFolder(), true);
} else if (entry instanceof BIFFResourceEntry) {
((BIFFResourceEntry)entry).setOverride(true);
if (overrideInOverride) {
treeModel.removeResourceEntry(entry, entry.getExtension());
treeModel.addResourceEntry(new FileResourceEntry(path, true), overrideFolder, true);
}
}
}
});
}
}
}
treeModel.sort();
}
private List<ResourceEntry> getResourcesInternal(String type, List<Path> extraDirs)
{
List<ResourceEntry> list;
ResourceTreeFolder bifNode = treeModel.getFolder(type);
if (bifNode != null) {
list = new ArrayList<ResourceEntry>(bifNode.getResourceEntries());
} else {
list = new ArrayList<ResourceEntry>();
}
int initsize = list.size();
// include extra folders
if (extraDirs == null) {
extraDirs = Profile.getProperty(Profile.Key.GET_GAME_EXTRA_FOLDERS);
}
extraDirs.forEach((path) -> {
ResourceTreeFolder extraNode = treeModel.getFolder(path.getFileName().toString());
if (extraNode != null) {
list.addAll(extraNode.getResourceEntries(type));
}
});
// include override folders
if (BrowserMenuBar.getInstance() != null && !BrowserMenuBar.getInstance().ignoreOverrides()) {
ResourceTreeFolder overrideNode = treeModel.getFolder(Profile.getOverrideFolderName());
if (overrideNode != null) {
list.addAll(overrideNode.getResourceEntries(type));
}
}
if (list.size() > initsize) {
Collections.sort(list);
}
return list;
}
private void saveCopyOfResourceInternal(ResourceEntry entry)
{
String fileName;
do {
fileName = JOptionPane.showInputDialog(NearInfinity.getInstance(), "Enter new filename",
"Add copy of " + entry.toString(),
JOptionPane.QUESTION_MESSAGE);
if (fileName != null) {
if (fileName.indexOf(".") == -1) {
fileName += '.' + entry.getExtension();
}
if (fileName.lastIndexOf('.') > 8) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(),
"Filenames can only be up to 8 characters long (not including the file extension).",
"Error", JOptionPane.ERROR_MESSAGE);
fileName = null;
}
if (resourceExists(fileName)) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "File already exists!",
"Error", JOptionPane.ERROR_MESSAGE);
fileName = null;
}
} else {
return;
}
} while (fileName == null);
// creating override folder in game directory if it doesn't exist
Path outPath = FileManager.query(Profile.getGameRoot(), Profile.getOverrideFolderName().toLowerCase(Locale.ENGLISH));
if (!Files.isDirectory(outPath)) {
try {
Files.createDirectory(outPath);
} catch (IOException e) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Could not create " + outPath + ".",
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
return;
}
}
Path outFile = outPath.resolve(fileName);
if (entry.getExtension().equalsIgnoreCase("bs")) {
outFile = FileManager.query(Profile.getGameRoot(), "Scripts", fileName);
}
if (Files.exists(outFile)) {
String options[] = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(NearInfinity.getInstance(), outFile + " exists. Overwrite?",
"Save resource", JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0)
return;
}
try {
ByteBuffer bb = entry.getResourceBuffer();
try (OutputStream os = StreamUtils.getOutputStream(outFile, true)) {
WritableByteChannel wbc = Channels.newChannel(os);
wbc.write(bb);
}
JOptionPane.showMessageDialog(NearInfinity.getInstance(), entry.toString() + " copied to " + outFile,
"Copy complete", JOptionPane.INFORMATION_MESSAGE);
ResourceEntry newEntry = new FileResourceEntry(outFile, !entry.getExtension().equalsIgnoreCase("bs"));
treeModel.addResourceEntry(newEntry, newEntry.getTreeFolder(), true);
treeModel.sort();
NearInfinity.getInstance().showResourceEntry(newEntry);
} catch (Exception e) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error while copying " + entry,
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
private boolean saveResourceInternal(Resource resource, Component parent)
{
if (!(resource instanceof Writeable)) {
JOptionPane.showMessageDialog(parent, "Resource not savable", "Error", JOptionPane.ERROR_MESSAGE);
return false;
}
ResourceEntry entry = resource.getResourceEntry();
if (entry == null) {
return false;
}
Path outPath;
if (entry instanceof BIFFResourceEntry) {
Path overridePath = FileManager.query(Profile.getGameRoot(), Profile.getOverrideFolderName());
if (!Files.isDirectory(overridePath)) {
try {
Files.createDirectory(overridePath);
} catch (IOException e) {
JOptionPane.showMessageDialog(parent, "Unable to create override folder.",
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
return false;
}
}
outPath = FileManager.query(overridePath, entry.toString());
((BIFFResourceEntry)entry).setOverride(true);
} else {
outPath = entry.getActualPath();
// extra step for saving resources from a read-only medium (such as DLCs)
if (!FileManager.isDefaultFileSystem(outPath)) {
outPath = Profile.getGameRoot().resolve(outPath.subpath(0, outPath.getNameCount()).toString());
if (outPath != null && !Files.exists(outPath.getParent())) {
try {
Files.createDirectories(outPath.getParent());
} catch (IOException e) {
JOptionPane.showMessageDialog(parent, "Unable to create folder: " + outPath.getParent(),
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
return false;
}
}
}
}
if (Files.exists(outPath)) {
outPath = outPath.toAbsolutePath();
String options[] = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(parent, outPath + " exists. Overwrite?", "Save resource",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
null, options, options[0]) == 0) {
if (BrowserMenuBar.getInstance().backupOnSave()) {
try {
Path bakPath = outPath.getParent().resolve(outPath.getFileName() + ".bak");
if (Files.isRegularFile(bakPath)) {
Files.delete(bakPath);
}
if (!Files.exists(bakPath)) {
Files.move(outPath, bakPath);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
return false;
}
}
try (OutputStream os = StreamUtils.getOutputStream(outPath, true)) {
((Writeable)resource).write(os);
} catch (IOException e) {
JOptionPane.showMessageDialog(parent, "Error while saving " + resource.getResourceEntry().toString(),
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
return false;
}
JOptionPane.showMessageDialog(parent, "File saved to \"" + outPath.toAbsolutePath() + '\"',
"Save complete", JOptionPane.INFORMATION_MESSAGE);
if (resource.getResourceEntry().getExtension().equals("IDS")) {
IdsMapCache.cacheInvalid(resource.getResourceEntry());
IdsBrowser idsbrowser = (IdsBrowser)ChildFrame.getFirstFrame(IdsBrowser.class);
if (idsbrowser != null) {
idsbrowser.refreshList();
}
Compiler.restartCompiler();
} else if (resource.getResourceEntry().toString().equalsIgnoreCase(Song2daBitmap.getTableName())) {
Song2daBitmap.resetSonglist();
} else if (resource.getResourceEntry().toString().equalsIgnoreCase(Summon2daBitmap.getTableName())) {
Summon2daBitmap.resetSummonTable();
} else if (resource.getResourceEntry().toString().equalsIgnoreCase(PriTypeBitmap.getTableName())) {
PriTypeBitmap.resetTypeTable();
} else if (resource.getResourceEntry().toString().equalsIgnoreCase(SecTypeBitmap.getTableName())) {
SecTypeBitmap.resetTypeTable();
}
return true;
}
}