diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml
index f1fda9b3a..5be229a07 100644
--- a/.github/workflows/ant.yml
+++ b/.github/workflows/ant.yml
@@ -23,7 +23,7 @@ jobs:
sed -i 's/debug="false"/debug="true"/' build.xml
ant -noinput -buildfile build.xml
- name: Upload artifact
- if: ${{ github.actor != 'NearInfinityBrowser' }}
+ if: ${{ github.repository == 'Argent77/NearInfinity' }}
uses: pyTooling/Actions/releaser@r0
with:
tag: nightly
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 000000000..fd85e7579
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,138 @@
+# This workflow will build a Java project with Apache Ant and upload the created artifacts
+# on pushes to the master branch.
+
+name: Java CD with Apache Ant
+
+on:
+ push:
+ branches: [ "master" ]
+ workflow_dispatch:
+ branches: [ "master", "devel" ]
+
+permissions:
+ contents: read
+
+jobs:
+ # Build and upload NearInfinity.jar
+ deploy-jar:
+ if: ${{ github.repository == 'Argent77/NearInfinity' }}
+ runs-on: ubuntu-latest
+ name: Build NearInfinity.jar
+ outputs:
+ ni_version: ${{ steps.ni-build.outputs.NI_VERSION }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 1.8
+ uses: actions/setup-java@v3
+ with:
+ java-version: '8'
+ distribution: 'temurin'
+
+ - name: Build with Ant
+ id: ni-build
+ run: |
+ ant -noinput -buildfile build.xml
+ echo "NI_VERSION=$(java -jar "NearInfinity.jar" -version 2>/dev/null | grep -Eo '[0-9]{8}')" >> "$GITHUB_OUTPUT"
+
+ - name: Upload JAR artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: NearInfinity-${{ steps.ni-build.outputs.NI_VERSION }}
+ path: NearInfinity.jar
+
+ # Build and upload installer versions for Windows and macOS
+ deploy-installer:
+ if: ${{ github.repository == 'Argent77/NearInfinity' }}
+ needs: deploy-jar
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ windows-latest, macos-latest ]
+ java: [ '21' ]
+ runs-on: ${{ matrix.os }}
+ name: Create installer for ${{ matrix.os }}, JDK ${{ matrix.java }}
+ steps:
+ # Initializations
+ - name: Git checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ java-version: ${{ matrix.java }}
+ distribution: 'temurin'
+
+ - name: Echo JAVA_HOME (windows)
+ if: (matrix.os == 'windows-latest')
+ run: |
+ echo $env:JAVA_HOME
+ java -version
+
+ - name: Echo JAVA_HOME (macos)
+ if: (matrix.os != 'windows-latest')
+ run: |
+ echo $JAVA_HOME
+ java -version
+
+ # Preparations
+ - name: Download JAR artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: NearInfinity-${{ needs.deploy-jar.outputs.ni_version }}
+ path: jar
+
+ - name: Set up installer data
+ uses: actions/checkout@v4
+ with:
+ repository: NearInfinityBrowser/NearInfinity-assets
+ path: assets
+
+ # Building
+ - name: Build portable archive and installer (windows)
+ if: (matrix.os == 'windows-latest')
+ run: |
+ move assets\redistributable\windows\package .
+ move assets\redistributable\windows\build-image.cmd .
+ move assets\redistributable\windows\build-installer.cmd .
+ .\build-image.cmd
+ .\build-installer.cmd
+
+ - name: Build installer (macos)
+ if: (matrix.os == 'macos-latest')
+ run: |
+ mv assets/redistributable/macos/package .
+ mv assets/redistributable/macos/build.command .
+ chmod +x build.command
+ ./build.command
+
+ # Validation
+ - name: List built files (windows)
+ if: (matrix.os == 'windows-latest')
+ run: dir
+
+ - name: List built files (macos)
+ if: (matrix.os == 'macos-latest')
+ run: ls -l
+
+ # Uploading
+ - name: Upload portable artifact (windows)
+ if: (matrix.os == 'windows-latest')
+ uses: actions/upload-artifact@v3
+ with:
+ name: portable-windows
+ path: NearInfinity-*.zip
+
+ - name: Upload exe artifact (windows)
+ if: (matrix.os == 'windows-latest')
+ uses: actions/upload-artifact@v3
+ with:
+ name: installer-windows
+ path: NearInfinity-*.exe
+
+ - name: Upload pkg artifact (macos)
+ if: (matrix.os == 'macos-latest')
+ uses: actions/upload-artifact@v3
+ with:
+ name: installer-macos
+ path: NearInfinity-*.pkg
diff --git a/src/org/infinity/AppOption.java b/src/org/infinity/AppOption.java
index c693eaaf9..16034647b 100644
--- a/src/org/infinity/AppOption.java
+++ b/src/org/infinity/AppOption.java
@@ -206,6 +206,9 @@ public class AppOption {
public static final AppOption BCS_INDENT = new AppOption(OptionsMenuItem.OPTION_BCS_INDENT, "Indentation", 1);
// Category: Misc. Types
+ /** Menu Options > Misc. Types: AutoAlign2da (Integer, Default: 0) */
+ public static final AppOption AUTO_ALIGN_2DA = new AppOption(OptionsMenuItem.OPTION_2DA_AUTOALIGN,
+ "Auto-Align 2DA Columns", 0);
/** Menu Options > Misc. Types: GlslColorScheme (Integer, Default: 0) */
public static final AppOption GLSL_COLOR_SCHEME = new AppOption(OptionsMenuItem.OPTION_GLSL_COLORSCHEME,
"GLSL Color Scheme", 0);
diff --git a/src/org/infinity/NearInfinity.java b/src/org/infinity/NearInfinity.java
index 716c9958e..f6517e91f 100644
--- a/src/org/infinity/NearInfinity.java
+++ b/src/org/infinity/NearInfinity.java
@@ -136,7 +136,7 @@
public final class NearInfinity extends JFrame implements ActionListener, ViewableContainer {
// the current Near Infinity version
- private static final String VERSION = "v2.4-20230729";
+ private static final String VERSION = "v2.4-20231231";
// the minimum supported Java version
private static final int JAVA_VERSION_MIN = 8;
@@ -247,6 +247,7 @@ private static Path findKeyfile() {
} else {
chooser = new JFileChooser(Profile.getGameRoot().toFile());
}
+ chooser.setFileHidingEnabled(false);
chooser.setDialogTitle("Open game: Locate keyfile");
chooser.setFileFilter(new FileFilter() {
@Override
@@ -888,7 +889,11 @@ public boolean removeViewable() {
}
public void showResourceEntry(ResourceEntry resourceEntry) {
- tree.select(resourceEntry);
+ showResourceEntry(resourceEntry, null);
+ }
+
+ public void showResourceEntry(ResourceEntry resourceEntry, Operation doneOperation) {
+ tree.select(resourceEntry, doneOperation);
}
public void quit() {
diff --git a/src/org/infinity/check/BCSIDSChecker.java b/src/org/infinity/check/BCSIDSChecker.java
index 30eb6a78d..dd931a119 100644
--- a/src/org/infinity/check/BCSIDSChecker.java
+++ b/src/org/infinity/check/BCSIDSChecker.java
@@ -65,9 +65,10 @@ public void actionPerformed(ActionEvent event) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- NearInfinity.getInstance().showResourceEntry(resourceEntry);
- BcsResource bcsfile = (BcsResource) NearInfinity.getInstance().getViewable();
- bcsfile.highlightText(((Integer) table.getValueAt(row, 2)), null);
+ NearInfinity.getInstance().showResourceEntry(resourceEntry, () -> {
+ final BcsResource bcsfile = (BcsResource) NearInfinity.getInstance().getViewable();
+ bcsfile.highlightText(((Integer) table.getValueAt(row, 2)), null);
+ });
}
} else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
diff --git a/src/org/infinity/check/CreInvChecker.java b/src/org/infinity/check/CreInvChecker.java
index f15a0baaf..23a8aeecf 100644
--- a/src/org/infinity/check/CreInvChecker.java
+++ b/src/org/infinity/check/CreInvChecker.java
@@ -68,9 +68,9 @@ public void actionPerformed(ActionEvent event) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- NearInfinity.getInstance().showResourceEntry(resourceEntry);
- ((AbstractStruct) NearInfinity.getInstance().getViewable()).getViewer()
- .selectEntry(((Item) table.getValueAt(row, 2)).getName());
+ NearInfinity.getInstance().showResourceEntry(resourceEntry,
+ () -> ((AbstractStruct)NearInfinity.getInstance().getViewable()).getViewer()
+ .selectEntry(((Item)table.getValueAt(row, 2)).getName()));
}
} else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
diff --git a/src/org/infinity/check/DialogChecker.java b/src/org/infinity/check/DialogChecker.java
index d8e5841f4..3d0915621 100644
--- a/src/org/infinity/check/DialogChecker.java
+++ b/src/org/infinity/check/DialogChecker.java
@@ -84,9 +84,10 @@ public void actionPerformed(ActionEvent event) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- NearInfinity.getInstance().showResourceEntry(resourceEntry);
- ((AbstractStruct) NearInfinity.getInstance().getViewable()).getViewer()
- .selectEntry((String) table.getValueAt(row, 1));
+ final SortableTable tableCapture = table;
+ NearInfinity.getInstance().showResourceEntry(resourceEntry,
+ () -> ((AbstractStruct)NearInfinity.getInstance().getViewable()).getViewer()
+ .selectEntry((String)tableCapture.getValueAt(row, 1)));
}
} else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
diff --git a/src/org/infinity/check/ScriptChecker.java b/src/org/infinity/check/ScriptChecker.java
index 93d56cf35..096dc2b99 100644
--- a/src/org/infinity/check/ScriptChecker.java
+++ b/src/org/infinity/check/ScriptChecker.java
@@ -76,9 +76,10 @@ public void actionPerformed(ActionEvent event) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- NearInfinity.getInstance().showResourceEntry(resourceEntry);
- ((BcsResource) NearInfinity.getInstance().getViewable()).highlightText(((Integer) table.getValueAt(row, 2)),
- null);
+ final SortableTable tableCapture = table;
+ NearInfinity.getInstance().showResourceEntry(resourceEntry,
+ () -> ((BcsResource)NearInfinity.getInstance().getViewable())
+ .highlightText(((Integer)tableCapture.getValueAt(row, 2)), null));
}
} else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
diff --git a/src/org/infinity/check/StructChecker.java b/src/org/infinity/check/StructChecker.java
index f740bea03..15abd7a2b 100644
--- a/src/org/infinity/check/StructChecker.java
+++ b/src/org/infinity/check/StructChecker.java
@@ -11,6 +11,7 @@
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -29,6 +30,7 @@
import org.infinity.datatype.IsNumeric;
import org.infinity.datatype.IsReference;
import org.infinity.datatype.IsTextual;
+import org.infinity.datatype.SectionOffset;
import org.infinity.datatype.StringRef;
import org.infinity.datatype.TextString;
import org.infinity.gui.Center;
@@ -39,12 +41,16 @@
import org.infinity.gui.menu.BrowserMenuBar;
import org.infinity.icon.Icons;
import org.infinity.resource.AbstractStruct;
+import org.infinity.resource.AddRemovable;
+import org.infinity.resource.Profile;
import org.infinity.resource.Resource;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.StructEntry;
+import org.infinity.resource.Viewable;
import org.infinity.resource.bcs.Compiler;
import org.infinity.resource.bcs.ScriptMessage;
import org.infinity.resource.bcs.ScriptType;
+import org.infinity.resource.cre.CreResource;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.resource.sto.ItemSale11;
import org.infinity.resource.wed.Overlay;
@@ -99,14 +105,12 @@ public void actionPerformed(ActionEvent event) {
if (event.getSource() == bopen) {
int row = table.getSelectedRow();
if (row != -1) {
- ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- NearInfinity.getInstance().showResourceEntry(resourceEntry);
+ showInViewer((Corruption) table.getTableItemAt(row), false);
}
} else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
if (row != -1) {
- ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- new ViewFrame(resultFrame, ResourceFactory.getResource(resourceEntry));
+ showInViewer((Corruption) table.getTableItemAt(row), true);
}
} else if (event.getSource() == bsave) {
table.saveCheckResult(resultFrame, "Corrupted files");
@@ -170,10 +174,7 @@ public void mouseReleased(MouseEvent event) {
if (event.getClickCount() == 2) {
final int row = table.getSelectedRow();
if (row != -1) {
- final ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0);
- final Resource resource = ResourceFactory.getResource(resourceEntry);
- new ViewFrame(resultFrame, resource);
- ((AbstractStruct) resource).getViewer().selectEntry((String) table.getValueAt(row, 1));
+ showInViewer((Corruption) table.getTableItemAt(row), true);
}
}
}
@@ -260,6 +261,67 @@ private void search(ResourceEntry entry, AbstractStruct struct) {
}
}
+ // Checking for valid substructure offsets and counts
+ // calculating size of static portion of the resource data
+ final HashSet> removableSet = new HashSet<>();
+ int headerSize = 0;
+ if (Profile.hasProperty(Profile.Key.IS_SUPPORTED_CRE_V22) && entry.getExtension().equalsIgnoreCase("CRE")) {
+ // special: CRE V2.2 static size cannot be determined dynamically
+ final String version = ((TextString) struct.getAttribute(AbstractStruct.COMMON_VERSION)).getText();
+ if ("V2.2".equalsIgnoreCase(version)) {
+ headerSize = 0x62e;
+ }
+ }
+ if (headerSize == 0) {
+ for (final StructEntry field : struct.getFields()) {
+ if (field instanceof SectionOffset) {
+ final Class extends StructEntry> cls = ((SectionOffset) field).getSection();
+ removableSet.add(cls);
+ }
+ if (field instanceof AddRemovable || removableSet.contains(field.getClass())) {
+ headerSize = field.getOffset();
+ break;
+ } else {
+ headerSize = field.getOffset() + field.getSize();
+ }
+ }
+ }
+ removableSet.clear();
+
+ // CHR offset correction for embedded CRE data
+ int ofsOffset = 0;
+ if (entry.getExtension().equalsIgnoreCase("CHR")) {
+ final StructEntry se = struct.getAttribute(CreResource.CHR_SIGNATURE_2);
+ if (se != null) {
+ ofsOffset = se.getOffset();
+ }
+ }
+
+ // checking offsets
+ for (final StructEntry field : struct.getFields()) {
+ if (field.getOffset() >= headerSize) {
+ break;
+ }
+ if (field instanceof SectionOffset) {
+ final SectionOffset so = (SectionOffset) field;
+ if (so.getValue() + ofsOffset < headerSize) {
+ synchronized (table) {
+ table.addTableItem(new Corruption(entry, so.getOffset(),
+ "Offset field points to header data (field name: \"" + so.getName() + "\", offset: "
+ + Integer.toHexString(so.getValue()) + "h, header size: "
+ + Integer.toHexString(headerSize - ofsOffset) + "h)"));
+ }
+ } else if (so.getValue() + ofsOffset > struct.getSize()) {
+ synchronized (table) {
+ table.addTableItem(new Corruption(entry, so.getOffset(),
+ "Offset field value is out of range (field name: \"" + so.getName() + "\", offset: "
+ + Integer.toHexString(so.getValue()) + "h, resource size: "
+ + Integer.toHexString(struct.getSize() - ofsOffset) + "h)"));
+ }
+ }
+ }
+ }
+
// Type-specific checks
if (entry.getExtension().equalsIgnoreCase("WED")) {
List list = getWedCorruption(entry, struct);
@@ -415,16 +477,71 @@ private List getWedCorruption(ResourceEntry entry, AbstractStruct st
}
return list;
}
+
+ /**
+ * Opens a view of the referenced resources and selects the field at the offset in question.
+ *
+ * @param corruption {@link Corruption} instance with error information.
+ * @param newWindow Whether to open the resource in a new window.
+ */
+ private void showInViewer(Corruption corruption, boolean newWindow) {
+ if (corruption == null) {
+ return;
+ }
+
+ if (newWindow) {
+ final ResourceEntry entry = corruption.getResourceEntry();
+ final Resource res = ResourceFactory.getResource(entry);
+ final int offset = corruption.getOffset();
+ new ViewFrame(resultFrame, res);
+ if (res instanceof AbstractStruct) {
+ try {
+ ((AbstractStruct) res).getViewer().selectEntry(offset);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ final ResourceEntry entry = corruption.getResourceEntry();
+ final int offset = corruption.getOffset();
+ NearInfinity.getInstance().showResourceEntry(entry);
+ if (parent instanceof ViewFrame && parent.isVisible()) {
+ final Resource res = ResourceFactory.getResource(entry);
+ ((ViewFrame) parent).setViewable(res);
+ if (res instanceof AbstractStruct) {
+ try {
+ ((AbstractStruct) res).getViewer().selectEntry(offset);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ NearInfinity.getInstance().showResourceEntry(entry, () -> {
+ final Viewable viewable = NearInfinity.getInstance().getViewable();
+ if (viewable instanceof AbstractStruct) {
+ try {
+ ((AbstractStruct) viewable).getViewer().selectEntry(offset);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ }
+ }
+
// -------------------------- INNER CLASSES --------------------------
private static final class Corruption implements TableItem {
private final ResourceEntry resourceEntry;
- private final String offset;
+ private final int offset;
+ private final String offsetString;
private final String errorMsg;
private Corruption(ResourceEntry resourceEntry, int offset, String errorMsg) {
this.resourceEntry = resourceEntry;
- this.offset = Integer.toHexString(offset) + 'h';
+ this.offset= offset;
+ this.offsetString = Integer.toHexString(offset) + 'h';
this.errorMsg = errorMsg;
}
@@ -433,15 +550,27 @@ public Object getObjectAt(int columnIndex) {
if (columnIndex == 0) {
return resourceEntry;
} else if (columnIndex == 1) {
- return offset;
+ return offsetString;
} else {
return errorMsg;
}
}
+ public ResourceEntry getResourceEntry() {
+ return resourceEntry;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public String getMessage() {
+ return errorMsg;
+ }
+
@Override
public String toString() {
- return "File: " + resourceEntry.getResourceName() + ", Offset: " + offset + ", Error: " + errorMsg;
+ return "File: " + resourceEntry.getResourceName() + ", Offset: " + offsetString + ", Error: " + errorMsg;
}
}
diff --git a/src/org/infinity/datatype/ResourceRef.java b/src/org/infinity/datatype/ResourceRef.java
index 99888d640..c84033364 100644
--- a/src/org/infinity/datatype/ResourceRef.java
+++ b/src/org/infinity/datatype/ResourceRef.java
@@ -4,6 +4,7 @@
package org.infinity.datatype;
+import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
@@ -38,8 +39,11 @@
import org.infinity.gui.menu.BrowserMenuBar;
import org.infinity.icon.Icons;
import org.infinity.resource.AbstractStruct;
+import org.infinity.resource.Closeable;
+import org.infinity.resource.Resource;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.key.ResourceEntry;
+import org.infinity.resource.sound.SoundResource;
import org.infinity.util.Misc;
import org.infinity.util.io.StreamUtils;
@@ -80,12 +84,18 @@ public class ResourceRef extends Datatype
/** Button that used to open editor of current selected element in the list. */
private JButton bView;
+ /** Button that used to play sound of current selected element in the list. */
+ private JButton bPlay;
+
/**
* GUI component that lists all available resources that can be set to this resource reference and have edit field for
* ability to enter resource reference manually.
*/
private TextListPanel list;
+ /** Contains the {@link Resource} of the currently selected resource reference. */
+ private Resource currentResource;
+
/**
* Returns a list of resource extensions that can be used to display associated icons.
* @return String set with file extensions (without leading dot).
@@ -117,6 +127,15 @@ public void actionPerformed(ActionEvent event) {
if (isEditable(selected)) {
new ViewFrame(list.getTopLevelAncestor(), ResourceFactory.getResource(selected.entry));
}
+ } else if (event.getSource() == bPlay) {
+ final ResourceRefEntry selected = list.getSelectedValue();
+ if (isSound(selected)) {
+ // prevent overlapping sound playback
+ closeResource(currentResource);
+ SoundResource res = (SoundResource) ResourceFactory.getResource(selected.entry);
+ res.playSound();
+ currentResource = res;
+ }
}
}
@@ -182,21 +201,41 @@ public void mouseClicked(MouseEvent event) {
bUpdate.setActionCommand(StructViewer.UPDATE_VALUE);
bView = new JButton("View/Edit", Icons.ICON_ZOOM_16.getIcon());
bView.addActionListener(this);
- bView.setEnabled(isEditable(list.getSelectedValue()));
+ bPlay = new JButton("Play", Icons.ICON_PLAY_16.getIcon());
+ bPlay.addActionListener(this);
list.addListSelectionListener(this);
+ setResourceEntryUpdated(list.getSelectedValue());
GridBagConstraints gbc = null;
JPanel panel = new JPanel(new GridBagLayout());
- gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 2, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH,
+ gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 5, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0);
panel.add(list, gbc);
- gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 1.0, GridBagConstraints.SOUTH, GridBagConstraints.HORIZONTAL,
+
+ // spacer keeps controls in the center
+ final JPanel spacerTop = new JPanel();
+ spacerTop.setMinimumSize(new Dimension());
+ gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+ new Insets(0, 0, 0, 0), 0, 0);
+ panel.add(spacerTop, gbc);
+
+ gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
new Insets(3, 6, 3, 0), 0, 0);
panel.add(bUpdate, gbc);
- gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 1.0, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL,
+ gbc = ViewerUtil.setGBC(gbc, 1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
new Insets(3, 6, 3, 0), 0, 0);
panel.add(bView, gbc);
+ gbc = ViewerUtil.setGBC(gbc, 1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
+ new Insets(24, 6, 3, 0), 0, 0);
+ panel.add(bPlay, gbc);
+
+ // spacer keeps controls in the center
+ final JPanel spacerBottom = new JPanel();
+ spacerTop.setMinimumSize(new Dimension());
+ gbc = ViewerUtil.setGBC(gbc, 1, 4, 1, 1, 0.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+ new Insets(0, 0, 0, 0), 0, 0);
+ panel.add(spacerBottom, gbc);
panel.setMinimumSize(Misc.getScaledDimension(DIM_MEDIUM));
panel.setPreferredSize(panel.getMinimumSize());
@@ -251,7 +290,7 @@ public boolean updateValue(AbstractStruct struct) {
@Override
public void valueChanged(ListSelectionEvent e) {
- bView.setEnabled(isEditable(list.getSelectedValue()));
+ setResourceEntryUpdated(list.getSelectedValue());
}
@Override
@@ -373,10 +412,35 @@ public boolean isLegalEntry(ResourceEntry entry) {
void addExtraEntries(List entries) {
}
+ private void setResourceEntryUpdated(ResourceRefEntry entry) {
+ closeResource(currentResource);
+ if (entry != null) {
+ bView.setEnabled(isEditable(entry));
+ bPlay.setEnabled(isSound(entry));
+ } else {
+ bView.setEnabled(false);
+ bPlay.setEnabled(false);
+ }
+ }
+
+ private void closeResource(Resource resource) {
+ if (resource instanceof Closeable) {
+ try {
+ ((Closeable) resource).close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
private boolean isEditable(ResourceRefEntry ref) {
return ref != null && ref != NONE && ref.entry != null;
}
+ private boolean isSound(ResourceRefEntry ref) {
+ return ref != null && ref != NONE && ref.entry != null && ref.entry.isSound();
+ }
+
private void setValue(String newValue) {
final String oldValue = NONE.name.equals(resname) ? null : resname;
resname = newValue;
diff --git a/src/org/infinity/gui/PreferencesDialog.java b/src/org/infinity/gui/PreferencesDialog.java
index 547e5fc1c..4d7465c49 100644
--- a/src/org/infinity/gui/PreferencesDialog.java
+++ b/src/org/infinity/gui/PreferencesDialog.java
@@ -95,6 +95,14 @@
* that were formerly presented as individual entries in the "Options" menu.
*/
public class PreferencesDialog extends JDialog {
+ /**
+ * Blacklisted L&F themes: These themes should not be listed in the Preferences dialog.
+ *
+ * List contains the fully qualified class names of L&F themes.
+ *
+ */
+ private static final List LOOK_AND_FEEL_BLACKLIST = Arrays.asList("javax.swing.plaf.nimbus.NimbusLookAndFeel");
+
/** Definition of category names. */
public enum Category {
DEFAULT(""),
@@ -277,6 +285,16 @@ public String toString() {
)
),
OptionCategory.create(Category.MISC_RESOURCE_TYPES,
+ OptionGroup.create("2DA",
+ OptionGroupBox.create(AppOption.AUTO_ALIGN_2DA.getName(), AppOption.AUTO_ALIGN_2DA.getLabel(),
+ "Choose how to to automatically align 2DA table columns when the resource is opened."
+ + "Disabled: Table data is not modified.
"
+ + "Compact: Column widths are calculated individually.
"
+ + "Uniform: Column widths are calculated evenly (comparable to Weidu's PRETTY_PRINT_2DA.)"
+ + "
Note: Formatting is discarded when the resource is closed unless the changes "
+ + "are explicitly saved.
",
+ 0, OptionsMenuItem.AutoAlign2da.values(), AppOption.AUTO_ALIGN_2DA)
+ ),
OptionGroup.create("GLSL",
OptionGroupBox.create(AppOption.GLSL_COLOR_SCHEME.getName(), AppOption.GLSL_COLOR_SCHEME.getLabel(),
"Select a color scheme for GLSL resources."
@@ -455,9 +473,7 @@ public String toString() {
OptionGroupBox.create(AppOption.LOOK_AND_FEEL_CLASS.getName(), AppOption.LOOK_AND_FEEL_CLASS.getLabel(),
"Choose a Look & Feel theme for the GUI."
+ "
Metal is the default L&F theme and provides the most consistent user experience. "
- + "It is available on all platforms.
"
- + "Note: It is not recommended to use the \"Nimbus\" L&F theme. The theme "
- + "initializes an incomplete set of UI properties, which can result in display errors.
",
+ + "It is available on all platforms.
",
0, new DataItem>[0], AppOption.LOOK_AND_FEEL_CLASS)
.setOnInit(this::lookAndFeelClassOnInit).setOnAccept(this::lookAndFeelClassOnAccept),
OptionGroupBox.create(AppOption.TEXT_FONT.getName(), AppOption.TEXT_FONT.getLabel(),
@@ -1379,6 +1395,12 @@ private void lookAndFeelClassOnInit(OptionGroupBox gb) {
LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
for (int i = 0, curIdx = 0; i < info.length; i++) {
final LookAndFeelInfo lf = info[i];
+
+ // check if theme is black-listed
+ if (lf != null && LOOK_AND_FEEL_BLACKLIST.contains(lf.getClassName())) {
+ continue;
+ }
+
try {
// L&F description is only available from class instance
final Class> cls = Class.forName(lf.getClassName());
diff --git a/src/org/infinity/gui/ResourceTree.java b/src/org/infinity/gui/ResourceTree.java
index c1e61779d..05f751c4e 100644
--- a/src/org/infinity/gui/ResourceTree.java
+++ b/src/org/infinity/gui/ResourceTree.java
@@ -68,6 +68,7 @@
import org.infinity.resource.key.ResourceTreeFolder;
import org.infinity.resource.key.ResourceTreeModel;
import org.infinity.util.IconCache;
+import org.infinity.util.Operation;
import org.infinity.util.io.FileEx;
import org.infinity.util.io.FileManager;
import org.infinity.util.io.StreamUtils;
@@ -192,24 +193,39 @@ public void select(ResourceEntry entry) {
}
public void select(ResourceEntry entry, boolean forced) {
+ select(entry, forced, null);
+ }
+
+ public void select(ResourceEntry entry, Operation doneOperation) {
+ select(entry, false, doneOperation);
+ }
+
+ public void select(ResourceEntry entry, boolean forced, Operation doneOperation) {
new SwingWorker() {
@Override
protected Void doInBackground() throws Exception {
- if (entry == null) {
- tree.clearSelection();
- } else if (forced || entry != shownResource) {
- TreePath tp = ResourceFactory.getResourceTreeModel().getPathToNode(entry);
- try {
- expandListener.treeWillExpand(new TreeExpansionEvent(tree, tp));
- tree.scrollPathToVisible(tp);
- tree.addSelectionPath(tp);
- tree.repaint();
- } finally {
- expandListener.treeExpanded(new TreeExpansionEvent(tree, tp));
- }
+ if (entry == null) {
+ tree.clearSelection();
+ } else if (forced || entry != shownResource) {
+ TreePath tp = ResourceFactory.getResourceTreeModel().getPathToNode(entry);
+ try {
+ expandListener.treeWillExpand(new TreeExpansionEvent(tree, tp));
+ tree.scrollPathToVisible(tp);
+ tree.addSelectionPath(tp);
+ tree.repaint();
+ } finally {
+ expandListener.treeExpanded(new TreeExpansionEvent(tree, tp));
}
+ }
return null;
}
+
+ @Override
+ protected void done() {
+ if (doneOperation != null) {
+ doneOperation.perform();
+ }
+ }
}.execute();
}
diff --git a/src/org/infinity/gui/TextListPanel.java b/src/org/infinity/gui/TextListPanel.java
index 77b29feda..e749f5de6 100644
--- a/src/org/infinity/gui/TextListPanel.java
+++ b/src/org/infinity/gui/TextListPanel.java
@@ -41,6 +41,7 @@
import org.infinity.NearInfinity;
import org.infinity.datatype.AbstractBitmap;
+import org.infinity.datatype.IwdRef;
import org.infinity.datatype.ResourceBitmap;
import org.infinity.datatype.ResourceRef;
import org.infinity.icon.Icons;
@@ -345,7 +346,8 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
} else if (value instanceof AbstractBitmap.FormattedData>) {
// resolving Resource Bitmap
final AbstractBitmap.FormattedData> fmt = (AbstractBitmap.FormattedData>) value;
- if (fmt.getParent() != null) {
+ // Limit icon preview to parent type: IwdRef
+ if (fmt.getParent() instanceof IwdRef) {
final AbstractBitmap> bmp = fmt.getParent();
Object o = bmp.getDataOf(fmt.getValue());
if (o instanceof ResourceBitmap.RefEntry) {
diff --git a/src/org/infinity/gui/menu/OptionsMenuItem.java b/src/org/infinity/gui/menu/OptionsMenuItem.java
index 86604f6f8..9a31ef589 100644
--- a/src/org/infinity/gui/menu/OptionsMenuItem.java
+++ b/src/org/infinity/gui/menu/OptionsMenuItem.java
@@ -58,6 +58,28 @@
* Handles Option menu items for the {@link BrowserMenuBar}.
*/
public class OptionsMenuItem extends JMenuItem implements ActionListener {
+ /** Alignment types available for 2DA resources. */
+ public enum AutoAlign2da {
+ /** Do not align columns. */
+ DISABLED("Disabled"),
+ /** Align columns individually. */
+ COMPACT("Compact"),
+ /** Align columns evenly (comparable to WeiDU's PRETTY_PRINT_2DA). */
+ UNIFORM("Uniform"),
+ ;
+
+ private final String label;
+
+ private AutoAlign2da(String label) {
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+
// Symbolic name for the default character set
private static final String DEFAULT_CHARSET = "Auto";
@@ -151,6 +173,7 @@ public class OptionsMenuItem extends JMenuItem implements ActionListener {
public static final String OPTION_BCS_CODEFOLDING = "BcsCodeFolding";
public static final String OPTION_BCS_AUTO_INDENT = "BcsAutoIndent";
public static final String OPTION_BCS_INDENT = "BcsIndent";
+ public static final String OPTION_2DA_AUTOALIGN = "AutoAlign2da";
public static final String OPTION_GLSL_SYNTAXHIGHLIGHTING = "GlslSyntaxHighlighting";
public static final String OPTION_GLSL_COLORSCHEME = "GlslColorScheme";
public static final String OPTION_GLSL_CODEFOLDING = "GlslCodeFolding";
@@ -648,6 +671,14 @@ public String getWeiDUColorScheme() {
return COLOR_SCHEME.get(idx).getPath();
}
+ public AutoAlign2da getAutoAlign2da() {
+ int idx = AppOption.AUTO_ALIGN_2DA.getIntValue();
+ if (idx >= 0 && idx < AutoAlign2da.values().length) {
+ return AutoAlign2da.values()[idx];
+ }
+ return AutoAlign2da.DISABLED;
+ }
+
/** Returns whether the dialog tree viewer shows icons in front of state and response entries. */
public boolean showDlgTreeIcons() {
return AppOption.DLG_SHOW_ICONS.getBoolValue();
diff --git a/src/org/infinity/resource/AbstractStruct.java b/src/org/infinity/resource/AbstractStruct.java
index f025e3462..920119d94 100644
--- a/src/org/infinity/resource/AbstractStruct.java
+++ b/src/org/infinity/resource/AbstractStruct.java
@@ -19,6 +19,8 @@
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.swing.JComponent;
@@ -588,6 +590,7 @@ public int addDatatype(AddRemovable addedEntry, int index) {
}
setStructChanged(true);
fireTableRowsInserted(index, index);
+ updateRemovableIndices(addedEntry.getClass());
return index;
}
@@ -947,6 +950,7 @@ public void removeDatatype(AddRemovable removedEntry, boolean removeRecurse) {
superStruct.datatypeRemovedInChild(this, removedEntry);
}
fireTableRowsDeleted(index, index);
+ updateRemovableIndices(removedEntry.getClass());
setStructChanged(true);
}
@@ -1047,6 +1051,52 @@ public void setStructChanged(boolean changed) {
}
}
+ /**
+ * Ensures that all {@link AddRemovable} of the specified type are properly indexed in sequential order.
+ *
+ * @param cls Class type of the AddRemovables to update.
+ */
+ public void updateRemovableIndices(Class extends AddRemovable> cls) {
+ if (cls == null) {
+ return;
+ }
+
+ final List fieldList = new ArrayList<>(getFields(cls));
+ if (fieldList.isEmpty()) {
+ return;
+ }
+ fieldList.sort((f1, f2) -> f1.getOffset() - f2.getOffset());
+
+ int minIndex = Integer.MAX_VALUE;
+ int maxIndex = Integer.MIN_VALUE;
+ final Pattern patName = Pattern.compile("^(.+) ([0-9]+)$");
+ for (int i = 0, size = fieldList.size(); i < size; i++) {
+ final StructEntry entry = fieldList.get(i);
+ final Matcher m = patName.matcher(entry.getName());
+ final String name;
+ if (m.matches()) {
+ // existing or copy-pasted entry
+ name = m.group(1);
+// System.out.printf("Existing/pasted: name=%s, newIndex=%d\n", entry.getName(), i);
+ } else {
+ // newly added entry
+ name = entry.getName();
+// System.out.printf("Newly added: name=%s, newIndex=%d\n", name, i);
+ }
+
+ final String newName = name + " " + i;
+ entry.setName(newName);
+
+ final int fieldIdx = fields.indexOf(entry);
+ minIndex = Math.min(minIndex, fieldIdx);
+ maxIndex = Math.max(maxIndex, fieldIdx);
+ }
+
+ if (minIndex != Integer.MAX_VALUE && maxIndex != Integer.MIN_VALUE) {
+ fireTableRowsUpdated(minIndex, maxIndex);
+ }
+ }
+
public String toMultiLineString() {
final StringBuilder sb = new StringBuilder(30 * fields.size());
for (final StructEntry e : fields) {
diff --git a/src/org/infinity/resource/Profile.java b/src/org/infinity/resource/Profile.java
index 809596f41..0a078e4e9 100644
--- a/src/org/infinity/resource/Profile.java
+++ b/src/org/infinity/resource/Profile.java
@@ -111,6 +111,8 @@ public enum Game {
IWDHowTotLM(Engine.IWD, "Icewind Dale: Trials of the Luremaster"),
/** Icewind Dale II */
IWD2(Engine.IWD2, "Icewind Dale II"),
+ /** Icewind Dale II: Enhanced Edition */
+ IWD2EE(Engine.IWD2, "Icewind Dale II: Enhanced Edition"),
/** Baldur's Gate: Enhanced Edition */
BG1EE(Engine.EE, "Baldur's Gate: Enhanced Edition"),
/** Baldur's Gate: Siege of Dragonspear */
@@ -512,6 +514,7 @@ public enum Key {
GAME_EXTRA_FOLDERS.put(Game.IWDHoW, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS)));
GAME_EXTRA_FOLDERS.put(Game.IWDHowTotLM, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS)));
GAME_EXTRA_FOLDERS.put(Game.IWD2, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS)));
+ GAME_EXTRA_FOLDERS.put(Game.IWD2EE, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS)));
GAME_EXTRA_FOLDERS.put(Game.BG1EE, new ArrayList<>(Arrays.asList(EE_EXTRA_FOLDERS)));
GAME_EXTRA_FOLDERS.put(Game.BG1SoD, new ArrayList<>(Arrays.asList(EE_EXTRA_FOLDERS)));
GAME_EXTRA_FOLDERS.put(Game.BG2EE, new ArrayList<>(Arrays.asList(EE_EXTRA_FOLDERS)));
@@ -533,6 +536,7 @@ public enum Key {
GAME_SAVE_FOLDERS.put(Game.IWDHoW, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS)));
GAME_SAVE_FOLDERS.put(Game.IWDHowTotLM, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS)));
GAME_SAVE_FOLDERS.put(Game.IWD2, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS)));
+ GAME_SAVE_FOLDERS.put(Game.IWD2EE, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS)));
GAME_SAVE_FOLDERS.put(Game.BG1EE, new ArrayList<>(Arrays.asList(EE_SAVE_FOLDERS)));
GAME_SAVE_FOLDERS.put(Game.BG1SoD, new ArrayList<>(Arrays.asList(EE_SAVE_FOLDERS)));
GAME_SAVE_FOLDERS.put(Game.BG2EE, new ArrayList<>(Arrays.asList(EE_SAVE_FOLDERS)));
@@ -1527,6 +1531,15 @@ private static void initDefaultGameBinaries() {
osMap.put(Platform.OS.WINDOWS, list);
DEFAULT_GAME_BINARIES.put(Game.IWD2, osMap);
+ // IWD2EE (Windows)
+ osMap = new EnumMap<>(Platform.OS.class);
+ osMap.put(Platform.OS.UNIX, emptyList);
+ osMap.put(Platform.OS.MAC_OS, emptyList);
+ list = new ArrayList<>();
+ list.add("iwd2ee.exe");
+ osMap.put(Platform.OS.WINDOWS, list);
+ DEFAULT_GAME_BINARIES.put(Game.IWD2EE, osMap);
+
// BG1EE (Linux, macOS, Windows)
osMap = new EnumMap<>(Platform.OS.class);
list = new ArrayList<>();
@@ -1828,7 +1841,9 @@ && getLuaValue(FileManager.query(gameRoots, "engine.lua"), "engine_mode", "0", f
if (ini != null && FileEx.create(ini).isFile()) {
addEntry(Key.GET_GAME_INI_FILE, Type.PATH, ini);
}
- } else if (game == Game.IWD2 || (FileEx.create(FileManager.query(gameRoots, "iwd2.exe")).isFile())
+ } else if (game == Game.IWD2 || game == Game.IWD2EE
+ || (FileEx.create(FileManager.query(gameRoots, "iwd2.exe")).isFile()
+ || FileEx.create(FileManager.query(gameRoots, "iwd2ee.exe")).isFile())
&& (FileEx.create(FileManager.query(gameRoots, "Data/Credits.mve")).isFile())) {
if (game == null) {
game = Game.IWD2;
@@ -1965,6 +1980,8 @@ && getLuaValue(FileManager.query(gameRoots, "engine.lua"), "engine_mode", "0", f
}
} else if (!isForced && game == Game.BG1 && ResourceFactory.resourceExists("DURLAG.MVE")) {
game = Game.BG1TotSC;
+ } else if (!isForced && game == Game.IWD2 && FileEx.create(FileManager.query(gameRoots, "IEex.dll")).isFile()) {
+ game = Game.IWD2EE;
}
// updating game type
@@ -2287,7 +2304,7 @@ private void initResourceTypes() {
addEntry(Key.IS_SUPPORTED_LOG, Type.BOOLEAN, true);
- addEntry(Key.IS_SUPPORTED_LUA, Type.BOOLEAN, isEnhancedEdition());
+ addEntry(Key.IS_SUPPORTED_LUA, Type.BOOLEAN, isEnhancedEdition() || game == Game.IWD2EE);
addEntry(Key.IS_SUPPORTED_MAZE, Type.BOOLEAN, game == Game.PSTEE);
diff --git a/src/org/infinity/resource/ResourceFactory.java b/src/org/infinity/resource/ResourceFactory.java
index c6d793c3a..eb9ae616e 100644
--- a/src/org/infinity/resource/ResourceFactory.java
+++ b/src/org/infinity/resource/ResourceFactory.java
@@ -164,10 +164,11 @@ public static Class extends Resource> getResourceType(ResourceEntry entry, Str
} else if (ext.equals("MUS")) {
cls = MusResource.class;
} else if (ext.equals("IDS") || ext.equals("2DA") || ext.equals("BIO") || ext.equals("RES") || ext.equals("TXT")
- || ext.equals("LOG") || // WeiDU log files
- (ext.equals("SRC") && Profile.getEngine() == Profile.Engine.IWD2)
- || (Profile.isEnhancedEdition() && (ext.equals("SQL") || ext.equals("GUI") || ext.equals("LUA")
- || ext.equals("MENU") || ext.equals("GLSL")))) {
+ || ext.equals("LOG") // WeiDU log files
+ || (ext.equals("SRC") && Profile.getEngine() == Profile.Engine.IWD2)
+ || (ext.equals("LUA") && (Profile.isEnhancedEdition() || Profile.getGame() == Profile.Game.IWD2EE))
+ || (Profile.isEnhancedEdition()
+ && (ext.equals("SQL") || ext.equals("GUI") || ext.equals("MENU") || ext.equals("GLSL")))) {
cls = PlainTextResource.class;
} else if (ext.equals("INI")) {
final boolean isPST = Profile.getEngine() == Profile.Engine.PST;
diff --git a/src/org/infinity/resource/key/ResourceEntry.java b/src/org/infinity/resource/key/ResourceEntry.java
index cdf316a13..e67976704 100644
--- a/src/org/infinity/resource/key/ResourceEntry.java
+++ b/src/org/infinity/resource/key/ResourceEntry.java
@@ -280,4 +280,10 @@ public boolean isVisible() {
public abstract ResourceTreeFolder getTreeFolder();
public abstract boolean hasOverride();
+
+ public boolean isSound() {
+ String extension = getExtension().toUpperCase();
+
+ return extension.equals("WAV") || extension.equals("MUS") || extension.equals("ACM");
+ }
}
diff --git a/src/org/infinity/resource/sound/SoundResource.java b/src/org/infinity/resource/sound/SoundResource.java
index 83b3731a8..214a8ee14 100644
--- a/src/org/infinity/resource/sound/SoundResource.java
+++ b/src/org/infinity/resource/sound/SoundResource.java
@@ -168,8 +168,10 @@ public void searchReferences(Component parent) {
@Override
public void run() {
- bPlay.setIcon(PLAY_ICONS.get(false));
- bStop.setEnabled(true);
+ if (bPlay != null) {
+ bPlay.setIcon(PLAY_ICONS.get(false));
+ bStop.setEnabled(true);
+ }
if (audioBuffer != null) {
final TimerElapsedTask timerTask = new TimerElapsedTask(250L);
try {
@@ -182,8 +184,10 @@ public void run() {
player.stopPlay();
timerTask.stop();
}
- bStop.setEnabled(false);
- bPlay.setIcon(PLAY_ICONS.get(true));
+ if (bPlay != null) {
+ bStop.setEnabled(false);
+ bPlay.setIcon(PLAY_ICONS.get(true));
+ }
}
// --------------------- End Interface Runnable ---------------------
@@ -294,10 +298,11 @@ private synchronized void setLoaded(boolean b) {
if (bPlay != null) {
bPlay.setEnabled(b);
bPlay.setIcon(PLAY_ICONS.get(true));
+
+ updateTimeLabel(0);
+ miConvert.setEnabled(b);
+ buttonPanel.getControlByType(PROPERTIES).setEnabled(true);
}
- updateTimeLabel(0);
- miConvert.setEnabled(b);
- buttonPanel.getControlByType(PROPERTIES).setEnabled(true);
}
private synchronized void setClosed(boolean b) {
@@ -448,16 +453,22 @@ public void stop() {
timer.cancel();
timer = null;
paused = false;
- updateTimeLabel(0L);
+ if (bPlay != null) {
+ updateTimeLabel(0L);
+ }
}
}
@Override
public void run() {
- if (!paused && timer != null && player != null && player.getDataLine() != null) {
+ if (!paused && timer != null && player != null && player.getDataLine() != null && bPlay != null) {
updateTimeLabel(player.getDataLine().getMicrosecondPosition() / 1000L);
}
}
+ }
+ public void playSound() {
+ loadAudio();
+ new Thread(this).start();
}
}
diff --git a/src/org/infinity/resource/text/PlainTextResource.java b/src/org/infinity/resource/text/PlainTextResource.java
index 5b0f99c77..fbe77f4c9 100644
--- a/src/org/infinity/resource/text/PlainTextResource.java
+++ b/src/org/infinity/resource/text/PlainTextResource.java
@@ -137,6 +137,26 @@ public static String trimSpaces(String text, boolean trailing, boolean leading)
return retVal;
}
+ /**
+ * Aligns table columns individually.
+ *
+ * @param text The text content with table columns.
+ * @return The aligned text. Returns {@code null} if {@code text} argument is {@code null}.
+ */
+ public static String alignTableColumnsCompact(String text) {
+ return alignTableColumns(text, 2, true, 4);
+ }
+
+ /**
+ * Aligns all table columns evenly, comparable to WeiDU's PRETTY_PRINT_2DA.
+ *
+ * @param text The text content with table columns.
+ * @return The aligned text. Returns {@code null} if {@code text} argument is {@code null}.
+ */
+ public static String alignTableColumnsUniform(String text) {
+ return alignTableColumns(text, 1, false, 1);
+ }
+
/**
* Aligns table columns to improve readability.
*
@@ -329,7 +349,7 @@ public PlainTextResource(ResourceEntry entry) throws Exception {
buffer = StaticSimpleXorDecryptor.decrypt(buffer, 2);
}
final Charset cs = Misc.getCharsetFrom(BrowserMenuBar.getInstance().getOptions().getSelectedCharset());
- text = StreamUtils.readString(buffer, buffer.limit(), cs);
+ text = applyTransformText(StreamUtils.readString(buffer, buffer.limit(), cs));
}
// --------------------- Begin Interface ActionListener ---------------------
@@ -416,9 +436,9 @@ public void itemStateChanged(ItemEvent event) {
if (bpmFormat.getSelectedItem() == miFormatTrim) {
setText(trimSpaces(editor.getText(), true, false));
} else if (bpmFormat.getSelectedItem() == miFormatAlignCompact) {
- setText(alignTableColumns(editor.getText(), 2, true, 4));
+ setText(alignTableColumnsCompact(editor.getText()));
} else if (bpmFormat.getSelectedItem() == miFormatAlignUniform) {
- setText(alignTableColumns(editor.getText(), 1, false, 1));
+ setText(alignTableColumnsUniform(editor.getText()));
} else if (bpmFormat.getSelectedItem() == miFormatSort) {
setText(sortTable(editor.getText(), true, entry.getResourceRef().equalsIgnoreCase("TRIGGER")));
}
@@ -638,4 +658,30 @@ private void setSyntaxHighlightingEnabled(InfinityTextArea edit, InfinityScrollP
pane.applyExtendedSettings(language);
}
}
+
+ private String applyTransformText(String data) {
+ if (data == null) {
+ return data;
+ }
+
+ final String ext = (entry != null) ? entry.getExtension() : "";
+ if (ext.equals("2DA")) {
+ return applyAutoAlign2da(data);
+ }
+
+ return data;
+ }
+
+ private String applyAutoAlign2da(String data) {
+ switch (BrowserMenuBar.getInstance().getOptions().getAutoAlign2da()) {
+ case COMPACT:
+ return alignTableColumnsCompact(data);
+ case UNIFORM:
+ return alignTableColumnsUniform(data);
+ default:
+ }
+
+ return data;
+ }
+
}
diff --git a/src/org/infinity/resource/wmp/ViewerMap.java b/src/org/infinity/resource/wmp/ViewerMap.java
index 1363294e7..7ba4e3c28 100644
--- a/src/org/infinity/resource/wmp/ViewerMap.java
+++ b/src/org/infinity/resource/wmp/ViewerMap.java
@@ -24,6 +24,8 @@
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
import javax.imageio.ImageIO;
@@ -39,6 +41,7 @@
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
+import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
@@ -167,6 +170,7 @@ private enum Direction {
listPanel = (StructListPanel) ViewerUtil.makeListPanel("Areas", wmpMap, AreaEntry.class,
AreaEntry.WMP_AREA_CURRENT, new WmpAreaListRenderer(mapIcons), listeners);
+ listPanel.getList().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
JScrollPane mapScroll = new JScrollPane(rcMap);
mapScroll.getVerticalScrollBar().setUnitIncrement(16);
mapScroll.getHorizontalScrollBar().setUnitIncrement(16);
@@ -230,7 +234,7 @@ private void showOverlays(boolean showIcons, boolean showIconLabels, boolean sho
showMapIconLabels();
}
if (showDistances) {
- showMapDistances(listPanel.getList().getSelectedIndex());
+ showMapDistances(listPanel.getList().getSelectedIndices());
}
}
showDot((AreaEntry) listPanel.getList().getSelectedValue(), false);
@@ -346,86 +350,122 @@ private void showMapIconLabels() {
}
}
- /** Displays all map distances from the specified area (by index). */
- private void showMapDistances(int areaIndex) {
- AreaEntry area = getAreaEntry(areaIndex, true);
- if (area != null) {
- final Direction[] srcDir = { Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST };
- final Color[] dirColor = { Color.GREEN, Color.RED, Color.CYAN, Color.YELLOW };
- final int[] links = new int[8];
- final int linkSize = 216; // size of a single area link structure
- int ofsLinkBase = ((IsNumeric) getEntry().getAttribute(MapEntry.WMP_MAP_OFFSET_AREA_LINKS)).getValue();
-
- links[0] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_NORTH)).getValue();
- links[1] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_NORTH)).getValue();
- links[2] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_WEST)).getValue();
- links[3] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_WEST)).getValue();
- links[4] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_SOUTH)).getValue();
- links[5] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_SOUTH)).getValue();
- links[6] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_EAST)).getValue();
- links[7] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_EAST)).getValue();
- for (int dir = 0; dir < srcDir.length; dir++) {
- Direction curDir = srcDir[dir];
- Point ptOrigin = getMapIconCoordinate(areaIndex, curDir, true);
- for (int dirIndex = 0, dirCount = links[dir * 2 + 1]; dirIndex < dirCount; dirIndex++) {
- int ofsLink = ofsLinkBase + (links[dir * 2] + dirIndex) * linkSize;
- AreaLink destLink = (AreaLink) area.getAttribute(ofsLink, false);
-
- if (destLink != null) {
- int dstAreaIndex = ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue();
- Flag flag = (Flag) destLink.getAttribute(AreaLink.WMP_LINK_DEFAULT_ENTRANCE);
- Direction dstDir = Direction.NORTH;
- if (flag.isFlagSet(1)) {
- dstDir = Direction.EAST;
- } else if (flag.isFlagSet(2)) {
- dstDir = Direction.SOUTH;
- } else if (flag.isFlagSet(3)) {
- dstDir = Direction.WEST;
+ /**
+ * Displays all map distances from the specified area (by index).
+ *
+ * @param areaIndices Sequence of map indices for showing distances. Specify no parameters to show distances for all
+ * available maps.
+ */
+ private void showMapDistances(int... areaIndices) {
+ final List areaIndicesList = new ArrayList<>();
+ if (areaIndices.length == 0) {
+ int numAreas = ((IsNumeric) mapEntry.getAttribute(MapEntry.WMP_MAP_NUM_AREAS)).getValue();
+ for (int i = 0; i < numAreas; i++) {
+ areaIndicesList.add(i);
+ }
+ } else {
+ for (final int idx : areaIndices) {
+ areaIndicesList.add(idx);
+ }
+ }
+
+ for (final int curAreaIndex : areaIndicesList) {
+ AreaEntry area = getAreaEntry(curAreaIndex, true);
+ if (area != null) {
+ final Direction[] srcDir = { Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST };
+ final Color[] dirColor = { Color.GREEN, Color.RED, Color.CYAN, Color.YELLOW };
+ final int[] links = new int[8];
+ final int linkSize = 216; // size of a single area link structure
+ int ofsLinkBase = ((IsNumeric) getEntry().getAttribute(MapEntry.WMP_MAP_OFFSET_AREA_LINKS)).getValue();
+
+ links[0] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_NORTH)).getValue();
+ links[1] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_NORTH)).getValue();
+ links[2] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_WEST)).getValue();
+ links[3] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_WEST)).getValue();
+ links[4] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_SOUTH)).getValue();
+ links[5] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_SOUTH)).getValue();
+ links[6] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_EAST)).getValue();
+ links[7] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_EAST)).getValue();
+ for (int dir = 0; dir < srcDir.length; dir++) {
+ Direction curDir = srcDir[dir];
+ Point ptOrigin = getMapIconCoordinate(curAreaIndex, curDir, true);
+ for (int dirIndex = 0, dirCount = links[dir * 2 + 1]; dirIndex < dirCount; dirIndex++) {
+ int ofsLink = ofsLinkBase + (links[dir * 2] + dirIndex) * linkSize;
+ AreaLink destLink = (AreaLink) area.getAttribute(ofsLink, false);
+
+ // finding corresponding travel distances between selected areas
+ if (destLink != null && areaIndices.length > 1) {
+ final int destAreaIdx = ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue();
+ final AreaEntry destArea = getAreaEntry(destAreaIdx, false);
+ boolean found = false;
+ if (destArea != null) {
+ found = areaIndicesList
+ .stream()
+ .filter(idx -> curAreaIndex != idx && destArea.equals(getAreaEntry(idx, true)))
+ .findAny()
+ .isPresent();
+ }
+ if (!found) {
+ destLink = null;
+ }
}
- Point ptTarget = getMapIconCoordinate(dstAreaIndex, dstDir, false);
-
- // checking for random encounters during travels
- boolean hasRandomEncounters = false;
- if (((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_PROBABILITY)).getValue() > 0) {
- for (int rnd = 1; rnd < 6; rnd++) {
- String rndArea = ((IsReference) destLink
- .getAttribute(String.format(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_AREA_FMT, rnd))).getResourceName();
- if (ResourceFactory.resourceExists(rndArea)) {
- hasRandomEncounters = true;
- break;
+
+ if (destLink != null) {
+ int dstAreaIndex = ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue();
+ Flag flag = (Flag) destLink.getAttribute(AreaLink.WMP_LINK_DEFAULT_ENTRANCE);
+ Direction dstDir = Direction.NORTH;
+ if (flag.isFlagSet(1)) {
+ dstDir = Direction.EAST;
+ } else if (flag.isFlagSet(2)) {
+ dstDir = Direction.SOUTH;
+ } else if (flag.isFlagSet(3)) {
+ dstDir = Direction.WEST;
+ }
+ Point ptTarget = getMapIconCoordinate(dstAreaIndex, dstDir, false);
+
+ // checking for random encounters during travels
+ boolean hasRandomEncounters = false;
+ if (((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_PROBABILITY)).getValue() > 0) {
+ for (int rnd = 1; rnd < 6; rnd++) {
+ String rndArea = ((IsReference) destLink
+ .getAttribute(String.format(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_AREA_FMT, rnd))).getResourceName();
+ if (ResourceFactory.resourceExists(rndArea)) {
+ hasRandomEncounters = true;
+ break;
+ }
}
}
- }
- Graphics2D g = ((BufferedImage) rcMap.getImage()).createGraphics();
- g.setFont(g.getFont().deriveFont(g.getFont().getSize2D() * 0.8f));
- try {
- // drawing line
- g.setColor(dirColor[dir]);
- if (hasRandomEncounters) {
- g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1.0f,
- new float[] { 6.0f, 4.0f }, 0.0f));
- } else {
- g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+ Graphics2D g = ((BufferedImage) rcMap.getImage()).createGraphics();
+ g.setFont(g.getFont().deriveFont(g.getFont().getSize2D() * 0.8f));
+ try {
+ // drawing line
+ g.setColor(dirColor[dir]);
+ if (hasRandomEncounters) {
+ g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1.0f,
+ new float[] { 6.0f, 4.0f }, 0.0f));
+ } else {
+ g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+ }
+ g.drawLine(ptOrigin.x, ptOrigin.y, ptTarget.x, ptTarget.y);
+
+ // printing travel time (in hours)
+ String duration = String.format("%d h",
+ ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_DISTANCE_SCALE)).getValue() * 4);
+ LineMetrics lm = g.getFont().getLineMetrics(duration, g.getFontRenderContext());
+ Rectangle2D rectText = g.getFont().getStringBounds(duration, g.getFontRenderContext());
+ int textX = ptOrigin.x + ((ptTarget.x - ptOrigin.x) - rectText.getBounds().width) / 3;
+ int textY = ptOrigin.y + ((ptTarget.y - ptOrigin.y) - rectText.getBounds().height) / 3;
+ int textWidth = rectText.getBounds().width;
+ int textHeight = rectText.getBounds().height;
+ g.setColor(Color.LIGHT_GRAY);
+ g.fillRect(textX - 2, textY, textWidth + 4, textHeight);
+ g.setColor(Color.BLUE);
+ g.drawString(duration, textX, textY + lm.getAscent() + lm.getLeading());
+ } finally {
+ g.dispose();
+ g = null;
}
- g.drawLine(ptOrigin.x, ptOrigin.y, ptTarget.x, ptTarget.y);
-
- // printing travel time (in hours)
- String duration = String.format("%d h",
- ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_DISTANCE_SCALE)).getValue() * 4);
- LineMetrics lm = g.getFont().getLineMetrics(duration, g.getFontRenderContext());
- Rectangle2D rectText = g.getFont().getStringBounds(duration, g.getFontRenderContext());
- int textX = ptOrigin.x + ((ptTarget.x - ptOrigin.x) - rectText.getBounds().width) / 2;
- int textY = ptOrigin.y + ((ptTarget.y - ptOrigin.y) - rectText.getBounds().height) / 2;
- int textWidth = rectText.getBounds().width;
- int textHeight = rectText.getBounds().height;
- g.setColor(Color.LIGHT_GRAY);
- g.fillRect(textX - 2, textY, textWidth + 4, textHeight);
- g.setColor(Color.BLUE);
- g.drawString(duration, textX, textY + lm.getAscent() + lm.getLeading());
- } finally {
- g.dispose();
- g = null;
}
}
}
diff --git a/src/org/infinity/search/ReferenceHitFrame.java b/src/org/infinity/search/ReferenceHitFrame.java
index ae290c188..609b46248 100644
--- a/src/org/infinity/search/ReferenceHitFrame.java
+++ b/src/org/infinity/search/ReferenceHitFrame.java
@@ -130,12 +130,13 @@ public void actionPerformed(ActionEvent event) {
((ViewFrame) parent).toFront();
}
} else {
- NearInfinity.getInstance().showResourceEntry(entry);
- Viewable viewable = NearInfinity.getInstance().getViewable();
- showEntryInViewer(row, viewable);
- if (viewable instanceof DlgResource) {
- NearInfinity.getInstance().toFront();
- }
+ NearInfinity.getInstance().showResourceEntry(entry, () -> {
+ Viewable viewable = NearInfinity.getInstance().getViewable();
+ showEntryInViewer(row, viewable);
+ if (viewable instanceof DlgResource) {
+ NearInfinity.getInstance().toFront();
+ }
+ });
}
}
} else if (event.getSource() == bopennew) {
diff --git a/src/org/infinity/search/TextHitFrame.java b/src/org/infinity/search/TextHitFrame.java
index 5c1a11f4a..08484ff87 100644
--- a/src/org/infinity/search/TextHitFrame.java
+++ b/src/org/infinity/search/TextHitFrame.java
@@ -119,11 +119,12 @@ public void actionPerformed(ActionEvent event) {
((TextResource) res).highlightText(((Integer) table.getValueAt(row, 2)), query);
}
} else {
- NearInfinity.getInstance().showResourceEntry(entry);
- Viewable viewable = NearInfinity.getInstance().getViewable();
- if (viewable instanceof TextResource) {
- ((TextResource) viewable).highlightText(((Integer) table.getValueAt(row, 2)), query);
- }
+ NearInfinity.getInstance().showResourceEntry(entry, () -> {
+ Viewable viewable = NearInfinity.getInstance().getViewable();
+ if (viewable instanceof TextResource) {
+ ((TextResource) viewable).highlightText(((Integer) table.getValueAt(row, 2)), query);
+ }
+ });
}
}
} else if (event.getSource() == bopennew) {
diff --git a/src/org/infinity/search/advanced/AdvancedSearch.java b/src/org/infinity/search/advanced/AdvancedSearch.java
index ef82f8f22..5796bf657 100644
--- a/src/org/infinity/search/advanced/AdvancedSearch.java
+++ b/src/org/infinity/search/advanced/AdvancedSearch.java
@@ -819,12 +819,13 @@ public void actionPerformed(ActionEvent event) {
if (row != -1) {
ResourceEntry entry = (ResourceEntry) listResults.getValueAt(row, 0);
if (entry != null) {
- NearInfinity.getInstance().showResourceEntry(entry);
- Viewable viewable = NearInfinity.getInstance().getViewable();
- showEntryInViewer(row, viewable);
- if (viewable instanceof DlgResource) {
- NearInfinity.getInstance().toFront();
- }
+ NearInfinity.getInstance().showResourceEntry(entry, () -> {
+ Viewable viewable = NearInfinity.getInstance().getViewable();
+ showEntryInViewer(row, viewable);
+ if (viewable instanceof DlgResource) {
+ NearInfinity.getInstance().toFront();
+ }
+ });
}
}
} else if (event.getSource() == bOpenNew) {