}) Map of "Equipped appearance" codes with associated
* descriptions. Map is generated on first call of {@code getEquippedAppearanceMap()}.
*/
GET_GAME_EQUIPPED_APPEARANCES,
@@ -249,6 +252,14 @@ public enum Key {
IS_GAME_EEEX,
/** Property: ({@code Boolean}) Has type of current game been forcibly set? */
IS_FORCED_GAME,
+ /** Property: ({@code Integer}) Returns the Infinity Animations installed version:
+ *
+ * 0: not installed
+ * 1: old IA format (v5 or earlier)
+ * 2: new format (v6 or later)
+ *
+ */
+ GET_INFINITY_ANIMATIONS,
/** Property: ({@code Boolean}) Are {@code 2DA} resources supported? */
IS_SUPPORTED_2DA,
@@ -2291,6 +2302,52 @@ private void initFeatures()
addEntry(Key.IS_GAME_EEEX, Type.BOOLEAN, Boolean.FALSE);
}
+ // Is Infinity Animations installed?
+ boolean isIAv1 = false;
+ boolean isIAv2 = false;
+ if (engine == Engine.BG2) {
+ Path exe = FileManager.queryExisting(getGameRoot(), "bgmain.exe");
+ if (exe != null) {
+ File exeFile = exe.toFile();
+ if (exeFile != null && exeFile.length() == 7839790L) {
+ try (RandomAccessFile raf = new RandomAccessFile(exeFile, "r")) {
+ // checking key signatures
+ final int[] sigCheckV1 = { 0x3db6d84, 0xc6004c48, 0x54464958, 0x004141de, 0xf9 };
+ final int[] sigCheckV2 = { 0x3db6d84, 0xc6004c48, 0x54464958, 0x0041412d, 0xf9 };
+ long ofs[] = { 0x40742cL, 0x40a8daL, 0x7536e7L, 0x407713L };
+ int sig[] = new int[ofs.length + 1];
+ for (int i = 0; i < ofs.length; i++) {
+ // reading int signatures
+ raf.seek(ofs[i]);
+ int b1 = raf.read();
+ int b2 = raf.read();
+ int b3 = raf.read();
+ int b4 = raf.read();
+ if ((b1 | b2 | b3 | b4) < 0) {
+ throw new EOFException();
+ }
+ sig[i] = b1 | (b2 << 8) | (b3 << 16) | (b4 << 24);
+ }
+
+ // reading byte signature
+ raf.seek(0x4595c9L);
+ sig[ofs.length] = raf.read();
+
+ isIAv1 = Arrays.equals(sig, sigCheckV1);
+ isIAv2 = Arrays.equals(sig, sigCheckV2);
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+ if (isIAv1) {
+ addEntry(Key.GET_INFINITY_ANIMATIONS, Type.INTEGER, Integer.valueOf(1)); // v5 or earlier
+ } else if (isIAv2) {
+ addEntry(Key.GET_INFINITY_ANIMATIONS, Type.INTEGER, Integer.valueOf(2)); // v6 or later
+ } else {
+ addEntry(Key.GET_INFINITY_ANIMATIONS, Type.INTEGER, Integer.valueOf(0)); // not installed
+ }
+
// Add campaign-specific extra folders
initCampaigns();
}
diff --git a/src/org/infinity/resource/cre/CreResource.java b/src/org/infinity/resource/cre/CreResource.java
index fe0478276..fbe6990a2 100644
--- a/src/org/infinity/resource/cre/CreResource.java
+++ b/src/org/infinity/resource/cre/CreResource.java
@@ -48,6 +48,7 @@
import org.infinity.datatype.UpdateListener;
import org.infinity.gui.ButtonPanel;
import org.infinity.gui.ButtonPopupMenu;
+import org.infinity.gui.ChildFrame;
import org.infinity.gui.StructViewer;
import org.infinity.gui.hexview.BasicColorMap;
import org.infinity.gui.hexview.StructHexViewer;
@@ -729,6 +730,16 @@ public CreResource(AbstractStruct superStruct, String name, ByteBuffer data, int
isChr = StreamUtils.readString(data, startoffset, 4).equalsIgnoreCase("CHR ");
}
+ @Override
+ public void close() throws Exception
+ {
+ ViewerAnimation va = ChildFrame.getFirstFrame(ViewerAnimation.class);
+ if (va != null) {
+ va.close();
+ }
+ super.close();
+ }
+
//
@Override
public AddRemovable[] getPrototypes() throws Exception
diff --git a/src/org/infinity/resource/cre/Viewer.java b/src/org/infinity/resource/cre/Viewer.java
index f52ca07be..2df8e902c 100644
--- a/src/org/infinity/resource/cre/Viewer.java
+++ b/src/org/infinity/resource/cre/Viewer.java
@@ -11,9 +11,12 @@
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
+import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
@@ -25,8 +28,10 @@
import org.infinity.datatype.Flag;
import org.infinity.datatype.IsNumeric;
import org.infinity.datatype.ResourceRef;
+import org.infinity.gui.ChildFrame;
import org.infinity.gui.ViewerUtil;
import org.infinity.gui.ViewerUtil.ListValueRenderer;
+import org.infinity.icon.Icons;
import org.infinity.resource.AbstractStruct;
import org.infinity.resource.Effect;
import org.infinity.resource.Effect2;
@@ -96,6 +101,7 @@ private static JPanel makeMiscPanelIWD2(CreResource cre)
ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_GENDER), gbl, gbc, true);
ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_ALIGNMENT), gbl, gbc, true);
ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_KIT), gbl, gbc, true);
+ ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_ANIMATION), gbl, gbc, true);
ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_CHALLENGE_RATING), gbl, gbc, true);
ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_SAVE_FORTITUDE), gbl, gbc, true);
ViewerUtil.addLabelFieldPair(panel, cre.getAttribute(CreResource.CRE_SAVE_REFLEX), gbl, gbc, true);
@@ -191,6 +197,24 @@ private JPanel makeMainPanel(CreResource cre)
imagePanel = ViewerUtil.makeImagePanel((ResourceRef)cre.getAttribute(CreResource.CRE_PORTRAIT_SMALL), true);
}
+ JButton bViewAnimation = new JButton("View creature animation", Icons.getIcon(Icons.ICON_VOLUME_16));
+ bViewAnimation.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ ViewerAnimation va = ChildFrame.getFirstFrame(ViewerAnimation.class);
+ if (va == null) {
+ va = new ViewerAnimation(cre);
+ } else if (!va.isVisible()) {
+ va.setVisible(true);
+ va.toFront();
+ } else {
+ va.toFront();
+ }
+ }
+ });
+ bViewAnimation.setMargin(new Insets(8, 8, 8, 4));
+
GridBagLayout gbl = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
JPanel leftPanel = new JPanel(gbl);
@@ -201,6 +225,8 @@ private JPanel makeMainPanel(CreResource cre)
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbl.setConstraints(imagePanel, gbc);
leftPanel.add(imagePanel);
+ gbl.setConstraints(bViewAnimation, gbc);
+ leftPanel.add(bViewAnimation);
gbc.weighty = 1.0;
gbl.setConstraints(effectPanel, gbc);
leftPanel.add(effectPanel);
diff --git a/src/org/infinity/resource/cre/ViewerAnimation.java b/src/org/infinity/resource/cre/ViewerAnimation.java
new file mode 100644
index 000000000..67f88d8e2
--- /dev/null
+++ b/src/org/infinity/resource/cre/ViewerAnimation.java
@@ -0,0 +1,517 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import javax.swing.BorderFactory;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+import javax.swing.JToggleButton;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import org.infinity.NearInfinity;
+import org.infinity.gui.ButtonPanel;
+import org.infinity.gui.Center;
+import org.infinity.gui.ChildFrame;
+import org.infinity.gui.RenderCanvas;
+import org.infinity.gui.ViewerUtil;
+import org.infinity.gui.WindowBlocker;
+import org.infinity.icon.Icons;
+import org.infinity.resource.cre.decoder.SpriteDecoder;
+import org.infinity.resource.cre.decoder.SpriteUtils;
+import org.infinity.resource.graphics.PseudoBamDecoder.PseudoBamControl;
+
+/**
+ * A basic creature animation viewer.
+ */
+public class ViewerAnimation extends ChildFrame implements ActionListener
+{
+ private static final Color TransparentColor = new Color(0, true);
+ private static final int ANIM_DELAY = 1000 / 15; // 15 fps in milliseconds
+
+ private static final ButtonPanel.Control CtrlNextCycle = ButtonPanel.Control.CUSTOM_1;
+ private static final ButtonPanel.Control CtrlPrevCycle = ButtonPanel.Control.CUSTOM_2;
+ private static final ButtonPanel.Control CtrlNextFrame = ButtonPanel.Control.CUSTOM_3;
+ private static final ButtonPanel.Control CtrlPrevFrame = ButtonPanel.Control.CUSTOM_4;
+ private static final ButtonPanel.Control CtrlPlay = ButtonPanel.Control.CUSTOM_5;
+ private static final ButtonPanel.Control CtrlCycleLabel = ButtonPanel.Control.CUSTOM_6;
+ private static final ButtonPanel.Control CtrlFrameLabel = ButtonPanel.Control.CUSTOM_7;
+ private static final ButtonPanel.Control CtrlSequenceLabel = ButtonPanel.Control.CUSTOM_8;
+ private static final ButtonPanel.Control CtrlSequenceList = ButtonPanel.Control.CUSTOM_9;
+ private static final ButtonPanel.Control CtrlShowCircle = ButtonPanel.Control.CUSTOM_10;
+ private static final ButtonPanel.Control CtrlShowSpace = ButtonPanel.Control.CUSTOM_11;
+ private static final ButtonPanel.Control CtrlZoom = ButtonPanel.Control.CUSTOM_12;
+
+ // List of potential sequences to display when loading a new creature
+ private static final List InitialSequences = new ArrayList() {{
+ add(SpriteDecoder.Sequence.STAND);
+ add(SpriteDecoder.Sequence.STAND2);
+ add(SpriteDecoder.Sequence.STAND3);
+ add(SpriteDecoder.Sequence.STAND_EMERGED);
+ add(SpriteDecoder.Sequence.PST_STAND);
+ add(SpriteDecoder.Sequence.STANCE);
+ add(SpriteDecoder.Sequence.STANCE2);
+ add(SpriteDecoder.Sequence.PST_STANCE);
+ add(SpriteDecoder.Sequence.WALK);
+ add(SpriteDecoder.Sequence.PST_WALK);
+ }};
+
+ private static boolean zoom = false;
+ private static boolean showSelectionCircle = false;
+ private static boolean showPersonalSpace = false;
+
+ private final ButtonPanel buttonControlPanel = new ButtonPanel();
+ private final CreResource cre;
+
+ private SpriteDecoder decoder;
+ private PseudoBamControl bamControl;
+ private RenderCanvas rcDisplay;
+ private int curCycle, curFrame;
+ private Timer timer;
+ private SpriteDecoder.Sequence sequence;
+
+ public ViewerAnimation(CreResource cre)
+ {
+ super("", true);
+ this.cre = Objects.requireNonNull(cre);
+ try {
+ this.decoder = SpriteDecoder.importSprite(getCre());
+
+ init();
+ } catch (Exception e) {
+ e.printStackTrace();
+ JOptionPane.showMessageDialog(this, "Creature animation could not be loaded.\nError message: " + e.getMessage(),
+ "Error", JOptionPane.ERROR_MESSAGE);
+ this.bamControl = null;
+ this.decoder = null;
+ close();
+ return;
+ }
+ }
+
+ public CreResource getCre()
+ {
+ return cre;
+ }
+
+ /** Returns the associated {@code SpriteDecoder} instance. */
+ public SpriteDecoder getDecoder()
+ {
+ return decoder;
+ }
+
+ /** Returns the {@code BamControl} instance linked to the {@code SpriteDecoder}. */
+ public PseudoBamControl getController()
+ {
+ return bamControl;
+ }
+
+ private void setController(PseudoBamControl ctrl)
+ {
+ this.bamControl = Objects.requireNonNull(ctrl, "BamControl cannot be null");
+ }
+
+ /** Returns the selected animation sequence. */
+ public SpriteDecoder.Sequence getAnimationSequence()
+ {
+ return sequence;
+ }
+
+ /** Loads a new animation sequence. */
+ private void setAnimationSequence(SpriteDecoder.Sequence seq) throws Exception
+ {
+ if (seq != null && seq != getAnimationSequence()) {
+ sequence = seq;
+ curFrame = 0;
+ getDecoder().loadSequence(seq);
+ resetAnimationSequence();
+ showFrame();
+ }
+ }
+
+ private void resetAnimationSequence() throws Exception
+ {
+ setController(getDecoder().createControl());
+ getController().setMode(PseudoBamControl.Mode.SHARED);
+ getController().setSharedPerCycle(false);
+ if (curCycle < getController().cycleCount()) {
+ getController().cycleSet(curCycle);
+ if (curFrame < getController().cycleFrameCount()) {
+ getController().cycleSetFrameIndex(curFrame);
+ }
+ }
+ curCycle = getController().cycleGet();
+ curFrame = getController().cycleGetFrameIndex();
+ updateCanvasSize();
+ }
+
+ public void updateCanvasSize()
+ {
+ int zoom = isZoomed() ? 2 : 1;
+ Dimension dim = getController().getSharedDimension();
+ Dimension dimDisplay = new Dimension(dim.width * zoom, dim.height * zoom);
+ boolean imageChanged = !dim.equals(new Dimension(rcDisplay.getImage().getWidth(null), rcDisplay.getImage().getHeight(null)));
+ boolean sizeChanged = !dimDisplay.equals(rcDisplay.getPreferredSize());
+ if (imageChanged || sizeChanged) {
+ rcDisplay.setImage(new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB));
+ if (sizeChanged) {
+ rcDisplay.setPreferredSize(dimDisplay);
+ Container c = SwingUtilities.getAncestorOfClass(JScrollPane.class, rcDisplay);
+ if (c != null) {
+ c.setMinimumSize(rcDisplay.getPreferredSize());
+ c.invalidate();
+ c.getParent().validate();
+ }
+ }
+ }
+ updateCanvas();
+ }
+
+ public void updateCanvas()
+ {
+ BufferedImage image = (BufferedImage)rcDisplay.getImage();
+ Graphics2D g = image.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Src);
+ g.setColor(TransparentColor);
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+ } finally {
+ g.dispose();
+ g = null;
+ }
+
+ // rendering new frame
+ getController().cycleGetFrame(image);
+ rcDisplay.repaint();
+ }
+
+ /** Returns whether animation is zoomed. */
+ public boolean isZoomed()
+ {
+ return ((JCheckBox)buttonControlPanel.getControlByType(CtrlZoom)).isSelected();
+ }
+
+ /** Returns whether the animation is played back. */
+ public boolean isPlaying()
+ {
+ if (timer == null) {
+ timer = new Timer(ANIM_DELAY, this);
+ }
+ return timer.isRunning();
+ }
+
+ /** Toggles playback between "play" and "pause". */
+ public void togglePlay()
+ {
+ if (isPlaying()) {
+ pause();
+ } else {
+ play();
+ }
+ }
+
+ /** Starts playback. Does nothing if animation is already played back. */
+ public void play()
+ {
+ if (!isPlaying()) {
+ timer.restart();
+ }
+ }
+
+ /** Stops playback. Does nothing if animation is already stopped. */
+ public void pause()
+ {
+ if (isPlaying()) {
+ timer.stop();
+ }
+ }
+
+ /** Rewinds animation of current cycle to first frame. */
+ public void rewind()
+ {
+ curFrame = 0;
+ showFrame();
+ }
+
+//--------------------- Begin Class ChildFrame ---------------------
+
+ @Override
+ protected boolean windowClosing(boolean forced) throws Exception
+ {
+ SpriteUtils.clearCache();
+ return true;
+ }
+
+//--------------------- End Class ChildFrame ---------------------
+
+//--------------------- Begin Interface ActionListener ---------------------
+
+ @Override
+ public void actionPerformed(ActionEvent event)
+ {
+ if (buttonControlPanel.getControlByType(CtrlSequenceList) == event.getSource()) {
+ JComboBox> cb = (JComboBox>)buttonControlPanel.getControlByType(CtrlSequenceList);
+ SpriteDecoder.Sequence seq = (SpriteDecoder.Sequence)(cb).getSelectedItem();
+ try {
+ WindowBlocker.blockWindow(this, true);
+ setAnimationSequence(seq);
+ updateControls();
+ } catch (Exception e) {
+ e.printStackTrace();
+ JOptionPane.showMessageDialog(this, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
+ cb.setSelectedItem(getAnimationSequence());
+ } finally {
+ WindowBlocker.blockWindow(this, false);
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlZoom) == event.getSource()) {
+ try {
+ WindowBlocker.blockWindow(this, true);
+ zoom = ((JCheckBox)buttonControlPanel.getControlByType(CtrlZoom)).isSelected();
+ updateCanvasSize();
+ } finally {
+ WindowBlocker.blockWindow(this, false);
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlShowCircle) == event.getSource()) {
+ try {
+ WindowBlocker.blockWindow(this, true);
+ showSelectionCircle = ((JCheckBox)buttonControlPanel.getControlByType(CtrlShowCircle)).isSelected();
+ getDecoder().setSelectionCircleEnabled(showSelectionCircle);
+ resetAnimationSequence();
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ WindowBlocker.blockWindow(this, false);
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlShowSpace) == event.getSource()) {
+ try {
+ WindowBlocker.blockWindow(this, true);
+ showPersonalSpace = ((JCheckBox)buttonControlPanel.getControlByType(CtrlShowSpace)).isSelected();
+ getDecoder().setPersonalSpaceVisible(showPersonalSpace);
+ resetAnimationSequence();
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ WindowBlocker.blockWindow(this, false);
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlPrevCycle) == event.getSource()) {
+ if (curCycle > 0) {
+ curCycle--;
+ getController().cycleSet(curCycle);
+ if (isPlaying() && getController().cycleFrameCount() == 0) {
+ pause();
+ ((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).setSelected(false);
+ }
+ rewind();
+ showFrame();
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlNextCycle) == event.getSource()) {
+ if (curCycle < getController().cycleCount() - 1) {
+ curCycle++;
+ getController().cycleSet(curCycle);
+ if (isPlaying() && getController().cycleFrameCount() == 0) {
+ pause();
+ ((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).setSelected(false);
+ }
+ rewind();
+ showFrame();
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlPrevFrame) == event.getSource()) {
+ if (curFrame > 0) {
+ curFrame--;
+ showFrame();
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlNextFrame) == event.getSource()) {
+ if (curFrame < getController().cycleFrameCount() - 1) {
+ curFrame++;
+ showFrame();
+ }
+ }
+ else if (buttonControlPanel.getControlByType(CtrlPlay) == event.getSource()) {
+ if (((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).isSelected()) {
+ play();
+ } else {
+ pause();
+ }
+ updateControls();
+ }
+ else if (timer == event.getSource()) {
+ curFrame += 1;
+ curFrame %= getController().cycleFrameCount();
+ showFrame();
+ }
+ }
+
+//--------------------- End Interface ActionListener ---------------------
+
+ private void init() throws Exception
+ {
+ Dimension dim = new Dimension(1, 1);
+ rcDisplay = new RenderCanvas(new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB));
+ rcDisplay.setHorizontalAlignment(SwingConstants.CENTER);
+ rcDisplay.setVerticalAlignment(SwingConstants.CENTER);
+ rcDisplay.setInterpolationType(RenderCanvas.TYPE_NEAREST_NEIGHBOR);
+ rcDisplay.setScalingEnabled(true);
+ JScrollPane scrollDisplay = new JScrollPane(rcDisplay, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ scrollDisplay.setBorder(BorderFactory.createEmptyBorder());
+
+ JToggleButton bPlay = new JToggleButton("Play", Icons.getIcon(Icons.ICON_PLAY_16));
+ bPlay.addActionListener(this);
+
+ JLabel lCycle = new JLabel("", JLabel.CENTER);
+ JButton bPrevCycle = new JButton(Icons.getIcon(Icons.ICON_BACK_16));
+ bPrevCycle.setMargin(new Insets(bPrevCycle.getMargin().top, 2, bPrevCycle.getMargin().bottom, 2));
+ bPrevCycle.addActionListener(this);
+ JButton bNextCycle = new JButton(Icons.getIcon(Icons.ICON_FORWARD_16));
+ bNextCycle.setMargin(bPrevCycle.getMargin());
+ bNextCycle.addActionListener(this);
+
+ JLabel lFrame = new JLabel("", JLabel.CENTER);
+ lFrame.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
+ JButton bPrevFrame = new JButton(Icons.getIcon(Icons.ICON_BACK_16));
+ bPrevFrame.setMargin(new Insets(bPrevFrame.getMargin().top, 2, bPrevFrame.getMargin().bottom, 2));
+ bPrevFrame.addActionListener(this);
+ JButton bNextFrame = new JButton(Icons.getIcon(Icons.ICON_FORWARD_16));
+ bNextFrame.setMargin(bPrevFrame.getMargin());
+ bNextFrame.addActionListener(this);
+
+ JLabel lSequence = new JLabel("Sequence:");
+ lSequence.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
+ DefaultComboBoxModel modelSequences = new DefaultComboBoxModel<>();
+ JComboBox cbSequences = new JComboBox<>(modelSequences);
+ cbSequences.addActionListener(this);
+ for (final SpriteDecoder.Sequence seq : SpriteDecoder.Sequence.values()) {
+ if (getDecoder().isSequenceAvailable(seq)) {
+ modelSequences.addElement(seq);
+ }
+ }
+ cbSequences.setEnabled(cbSequences.getItemCount() > 0);
+
+ JCheckBox cbZoom = new JCheckBox("Zoom", zoom);
+ cbZoom.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
+ cbZoom.addActionListener(this);
+ getDecoder().setSelectionCircleEnabled(showSelectionCircle);
+ JCheckBox cbShowCircle = new JCheckBox("Show selection circle", getDecoder().isSelectionCircleEnabled());
+ cbShowCircle.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
+ cbShowCircle.addActionListener(this);
+ getDecoder().setPersonalSpaceVisible(showPersonalSpace);
+ JCheckBox cbShowSpace = new JCheckBox("Show personal space", getDecoder().isPersonalSpaceVisible());
+ cbShowSpace.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
+ cbShowSpace.addActionListener(this);
+
+ buttonControlPanel.addControl(lCycle, CtrlCycleLabel);
+ buttonControlPanel.addControl(bPrevCycle, CtrlPrevCycle);
+ buttonControlPanel.addControl(bNextCycle, CtrlNextCycle);
+ buttonControlPanel.addControl(lFrame, CtrlFrameLabel);
+ buttonControlPanel.addControl(bPrevFrame, CtrlPrevFrame);
+ buttonControlPanel.addControl(bNextFrame, CtrlNextFrame);
+ buttonControlPanel.addControl(bPlay, CtrlPlay);
+ buttonControlPanel.addControl(lSequence, CtrlSequenceLabel);
+ buttonControlPanel.addControl(cbSequences, CtrlSequenceList);
+ buttonControlPanel.addControl(cbZoom, CtrlZoom);
+ buttonControlPanel.addControl(cbShowCircle, CtrlShowCircle);
+ buttonControlPanel.addControl(cbShowSpace, CtrlShowSpace);
+
+ setLayout(new GridBagLayout());
+ GridBagConstraints c;
+ c = ViewerUtil.setGBC(null, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
+ GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
+ add(scrollDisplay, c);
+ c = ViewerUtil.setGBC(null, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
+ GridBagConstraints.HORIZONTAL, new Insets(8, 0, 8, 0), 0, 0);
+ add(buttonControlPanel, c);
+
+ String name = getCre().getName();
+ if (!name.isEmpty()) {
+ setTitle(String.format("%s (%s)", getCre().getName(), getCre().getAttribute(CreResource.CRE_NAME)));
+ } else {
+ setTitle(getCre().getName());
+ }
+ setSize(NearInfinity.getInstance().getPreferredSize());
+ Center.center(this, NearInfinity.getInstance().getBounds());
+ setExtendedState(NearInfinity.getInstance().getExtendedState() & ~ICONIFIED);
+ setVisible(true);
+
+ // loading animation sequence
+ if (cbSequences.isEnabled()) {
+ int seqIdx = 0;
+ for (final SpriteDecoder.Sequence sequence : InitialSequences) {
+ int idx = ((DefaultComboBoxModel>)cbSequences.getModel()).getIndexOf(sequence);
+ if (idx >= 0) {
+ seqIdx = idx;
+ break;
+ }
+ }
+ SpriteDecoder.Sequence seq = cbSequences.getModel().getElementAt(seqIdx);
+ cbSequences.setSelectedItem(seq);
+ setAnimationSequence(seq);
+ }
+ }
+
+ private void showFrame()
+ {
+ if (getController() == null) {
+ return;
+ }
+
+ if (!getController().cycleSetFrameIndex(curFrame)) {
+ getController().cycleReset();
+ curFrame = 0;
+ }
+
+ updateCanvas();
+
+ ((JLabel)buttonControlPanel.getControlByType(CtrlCycleLabel))
+ .setText("Cycle: " + curCycle + "/" + (getController().cycleCount() - 1));
+ ((JLabel)buttonControlPanel.getControlByType(CtrlFrameLabel))
+ .setText("Frame: " + curFrame + "/" + (getController().cycleFrameCount() - 1));
+ updateControls();
+ }
+
+ private void updateControls()
+ {
+ if (getController() != null) {
+ buttonControlPanel.getControlByType(CtrlPrevFrame).setEnabled(curFrame > 0);
+ buttonControlPanel.getControlByType(CtrlPrevCycle).setEnabled(curCycle > 0);
+ buttonControlPanel.getControlByType(CtrlNextFrame).setEnabled(curFrame < getController().cycleFrameCount() - 1);
+ buttonControlPanel.getControlByType(CtrlNextCycle).setEnabled(curCycle < getController().cycleCount() - 1);
+ buttonControlPanel.getControlByType(CtrlPlay).setEnabled(getController().cycleFrameCount() > 0);
+ } else {
+ buttonControlPanel.getControlByType(CtrlPrevFrame).setEnabled(false);
+ buttonControlPanel.getControlByType(CtrlPrevCycle).setEnabled(false);
+ buttonControlPanel.getControlByType(CtrlNextFrame).setEnabled(false);
+ buttonControlPanel.getControlByType(CtrlNextCycle).setEnabled(false);
+ buttonControlPanel.getControlByType(CtrlPlay).setEnabled(false);
+ }
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/AmbientDecoder.java b/src/org/infinity/resource/cre/decoder/AmbientDecoder.java
new file mode 100644
index 000000000..762bc9027
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/AmbientDecoder.java
@@ -0,0 +1,163 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type C000 (ambient) animations.
+ * Available ranges: [c000,cfff]
+ */
+public class AmbientDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.AMBIENT;
+
+ public static final DecoderAttribute KEY_INVULNERABLE = DecoderAttribute.with("invulnerable", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_PATH_SMOOTH = DecoderAttribute.with("path_smooth", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_LIST_TYPE = DecoderAttribute.with("list_type", DecoderAttribute.DataType.INT);
+
+ /** Creature behaves normally. */
+ public static final int LISTTYPE_NORMAL = 0;
+ /**
+ * Creature behaves like a "flying" creatures:
+ * detected_by_infravision=0, {@link Sequence#WALK} transforms into {@link Sequence#STANCE},
+ * hardcoded exceptions (e.g. polymorph, pathfinding, ...)
+ */
+ public static final int LISTTYPE_FLYING = 2;
+
+ private static final HashMap> suffixMap = new HashMap>() {{
+ put(Sequence.WALK, Couple.with("G1", 0));
+ put(Sequence.STANCE, Couple.with("G1", 8));
+ put(Sequence.STAND, Couple.with("G1", 16));
+ put(Sequence.GET_HIT, Couple.with("G1", 24));
+ put(Sequence.DIE, Couple.with("G1", 32));
+ put(Sequence.SLEEP, get(Sequence.DIE));
+ put(Sequence.GET_UP, Couple.with("!G1", 32));
+ put(Sequence.TWITCH, Couple.with("G1", 40));
+ }};
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[ambient]");
+ lines.add("false_color=" + falseColor);
+ lines.add("resref=" + resref);
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public AmbientDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public AmbientDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns whether the creature is invulnerable by default. */
+ public boolean isInvulnerable() { return getAttribute(KEY_INVULNERABLE); }
+ protected void setInvulnerable(boolean b) { setAttribute(KEY_INVULNERABLE, b); }
+
+ /** ??? */
+ public boolean isSmoothPath() { return getAttribute(KEY_PATH_SMOOTH); }
+ protected void setSmoothPath(boolean b) { setAttribute(KEY_PATH_SMOOTH, b); }
+
+ /** ??? */
+ public int getListType() { return getAttribute(KEY_LIST_TYPE); }
+ protected void setListType(int v) { setAttribute(KEY_LIST_TYPE, v); }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ String resref = getAnimationResref();
+ ArrayList retVal = new ArrayList() {{
+ add(resref + "G1.BAM");
+ add(resref + "G1E.BAM");
+ if (!essential) {
+ add(resref + "G2.BAM");
+ add(resref + "G2E.BAM");
+ }
+ }};
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ setInvulnerable(section.getAsInteger(KEY_INVULNERABLE.getName(), 0) != 0);
+ setSmoothPath(section.getAsInteger(KEY_PATH_SMOOTH.getName(), 0) != 0);
+ setListType(section.getAsInteger(KEY_LIST_TYPE.getName(), 0));
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+ String resref = getAnimationResref();
+ Couple data = suffixMap.get(seq);
+ if (data != null) {
+ String suffix = data.getValue0();
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(suffix);
+ suffix = SegmentDef.fixBehaviorSuffix(suffix);
+ ResourceEntry entry = ResourceFactory.getResourceEntry(resref + suffix + ".BAM");
+ ResourceEntry entryE = ResourceFactory.getResourceEntry(resref + suffix + "E.BAM");
+ int cycle = data.getValue1().intValue();
+ int cycleE = cycle + SeqDef.DIR_REDUCED_W.length;
+ if (SpriteUtils.bamCyclesExist(entry, cycle, SeqDef.DIR_REDUCED_W.length) &&
+ SpriteUtils.bamCyclesExist(entryE, cycleE, SeqDef.DIR_REDUCED_E.length)) {
+ retVal = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_W, false, entry, cycle, null, behavior);
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_E, false, entryE, cycleE, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/AmbientStaticDecoder.java b/src/org/infinity/resource/cre/decoder/AmbientStaticDecoder.java
new file mode 100644
index 000000000..de51dd124
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/AmbientStaticDecoder.java
@@ -0,0 +1,141 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type B000 (ambient_static) animations.
+ * Available ranges: [b000,bfff]
+ */
+public class AmbientStaticDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.AMBIENT_STATIC;
+
+ public static final DecoderAttribute KEY_INVULNERABLE = DecoderAttribute.with("invulnerable", DecoderAttribute.DataType.BOOLEAN);
+
+ private static final HashMap> suffixMap = new HashMap>() {{
+ put(Sequence.STANCE, Couple.with("G1", 0));
+ put(Sequence.STAND, Couple.with("G1", 8));
+ put(Sequence.GET_HIT, Couple.with("G1", 16));
+ put(Sequence.DIE, Couple.with("G1", 24));
+ put(Sequence.SLEEP, get(Sequence.DIE));
+ put(Sequence.GET_UP, Couple.with("!G1", 24));
+ put(Sequence.TWITCH, Couple.with("G1", 32));
+ }};
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[ambient_static]");
+ lines.add("false_color=" + falseColor);
+ lines.add("resref=" + resref);
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public AmbientStaticDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public AmbientStaticDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns whether the creature is invulnerable by default. */
+ public boolean isInvulnerable() { return getAttribute(KEY_INVULNERABLE); }
+ protected void setInvulnerable(boolean b) { setAttribute(KEY_INVULNERABLE, b); }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ String resref = getAnimationResref();
+ ArrayList retVal = new ArrayList() {{
+ add(resref + "G1.BAM");
+ add(resref + "G1E.BAM");
+ if (!essential) {
+ add(resref + "G2.BAM");
+ add(resref + "G2E.BAM");
+ }
+ }};
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ setInvulnerable(section.getAsInteger(KEY_INVULNERABLE.getName(), 0) != 0);
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+ String resref = getAnimationResref();
+ Couple data = suffixMap.get(seq);
+ if (data != null) {
+ String suffix = data.getValue0();
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(suffix);
+ suffix = SegmentDef.fixBehaviorSuffix(suffix);
+ ResourceEntry entry = ResourceFactory.getResourceEntry(resref + suffix + ".BAM");
+ int cycle = data.getValue1().intValue();
+ ResourceEntry entryE = ResourceFactory.getResourceEntry(resref + suffix + "E.BAM");
+ int cycleE = cycle + SeqDef.DIR_REDUCED_W.length;
+ if (SpriteUtils.bamCyclesExist(entry, cycle, SeqDef.DIR_REDUCED_W.length) &&
+ SpriteUtils.bamCyclesExist(entryE, cycleE, SeqDef.DIR_REDUCED_E.length)) {
+ retVal = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_W, false, entry, cycle, null, behavior);
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_E, false, entryE, cycleE, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/ArmoredBaseDecoder.java b/src/org/infinity/resource/cre/decoder/ArmoredBaseDecoder.java
new file mode 100644
index 000000000..fda3411c7
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/ArmoredBaseDecoder.java
@@ -0,0 +1,249 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import org.infinity.datatype.IsNumeric;
+import org.infinity.datatype.IsTextual;
+import org.infinity.resource.Profile;
+import org.infinity.resource.StructEntry;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.itm.Ability;
+import org.infinity.resource.itm.ItmResource;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+
+/**
+ * Common base for processing creature animations with different armor levels.
+ */
+public abstract class ArmoredBaseDecoder extends SpriteDecoder
+{
+ public static final DecoderAttribute KEY_CAN_LIE_DOWN = DecoderAttribute.with("can_lie_down", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_DOUBLE_BLIT = DecoderAttribute.with("double_blit", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_EQUIP_HELMET = DecoderAttribute.with("equip_helmet", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_ARMOR_MAX_CODE = DecoderAttribute.with("armor_max_code", DecoderAttribute.DataType.INT);
+ public static final DecoderAttribute KEY_HEIGHT_CODE = DecoderAttribute.with("height_code", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_HEIGHT_CODE_HELMET = DecoderAttribute.with("height_code_helmet", DecoderAttribute.DataType.STRING);
+
+ /** Available attack types associated with attack sequences. */
+ public enum AttackType {
+ ONE_HANDED, TWO_HANDED, TWO_WEAPON, THROWING,
+ BOW, CROSSBOW, SLING
+ }
+
+ public ArmoredBaseDecoder(AnimationType type, int animationId, IniMap ini) throws Exception
+ {
+ super(type, animationId, ini);
+ }
+
+ public ArmoredBaseDecoder(AnimationType type, CreResource cre) throws Exception
+ {
+ super(type, cre);
+ }
+
+ /** Returns whether the creature falls down when dead/unconscious. */
+ public boolean canLieDown() { return getAttribute(KEY_CAN_LIE_DOWN); }
+ protected void setCanLieDown(boolean b) { setAttribute(KEY_CAN_LIE_DOWN, b); }
+
+ /** unused */
+ public boolean isDoubleBlit() { return getAttribute(KEY_DOUBLE_BLIT); }
+ protected void setDoubleBlit(boolean b) { setAttribute(KEY_DOUBLE_BLIT, b); }
+
+ /**
+ * Returns the maximum armor code value used as suffix in animation filenames.
+ * Highest code value is usually used by ArmorSpecificResref().
+ */
+ public int getMaxArmorCode() { return getAttribute(KEY_ARMOR_MAX_CODE); }
+ protected void setMaxArmorCode(int v) { setAttribute(KEY_ARMOR_MAX_CODE, Math.max(0, v)); }
+
+ /** Returns whether helmet overlay is shown. */
+ public boolean isHelmetEquipped() { return getAttribute(KEY_EQUIP_HELMET); }
+ protected void setHelmetEquipped(boolean b) { setAttribute(KEY_EQUIP_HELMET, b); }
+
+ /** Returns the height code prefix for helmet overlay sprites. Falls back to generic height code if needed. */
+ public String getHelmetHeightCode()
+ {
+ String retVal = getAttribute(KEY_HEIGHT_CODE_HELMET);
+ if (retVal.isEmpty()) {
+ retVal = getHeightCode();
+ }
+ return retVal;
+ }
+
+ protected void setHelmetHeightCode(String s) { setAttribute(KEY_HEIGHT_CODE_HELMET, s); }
+
+ /** Returns the creature animation height code prefix. */
+ public String getHeightCode() { return getAttribute(KEY_HEIGHT_CODE); }
+ protected void setHeightCode(String s)
+ {
+ if (s == null || s.isEmpty()) {
+ // heuristically determine height code
+ s = guessHeightCode();
+ }
+ setAttribute(KEY_HEIGHT_CODE, s);
+ }
+
+ /** Returns the armor code based on equipped armor of the current creature. */
+ public int getArmorCode()
+ {
+ int retVal = 1;
+ ItmResource itm = SpriteUtils.getEquippedArmor(getCreResource());
+ if (itm != null) {
+ String code = ((IsTextual)itm.getAttribute(ItmResource.ITM_EQUIPPED_APPEARANCE)).getText();
+ try {
+ retVal = Math.max(1, Math.min(getMaxArmorCode(), Integer.parseInt(code.substring(0, 1))));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return retVal;
+ }
+
+ /**
+ * Determines the attack type based on the specified item resource.
+ * @param itm the item resource.
+ * @param abilityIndex the item-specific ability to check (e.g. throwing or melee for throwing axes)
+ * @param preferTwoWeapon whether {@code AttackType.TwoWeapon} should be returned if a melee one-handed weapon is detected.
+ * @return attack type associated with the item resource.
+ */
+ public AttackType getAttackType(ItmResource itm, int abilityIndex, boolean preferTwoWeapon)
+ {
+ AttackType retVal = AttackType.ONE_HANDED;
+ if (itm == null) {
+ return retVal;
+ }
+
+ // collecting data
+ int flags = ((IsNumeric)itm.getAttribute(ItmResource.ITM_FLAGS)).getValue();
+ boolean isTwoHanded = (flags & (1 << 1)) != 0;
+ if (Profile.isEnhancedEdition()) {
+ // include fake two-handed weapons (e.g. monk fists)
+ isTwoHanded |= (flags & (1 << 12)) != 0;
+ }
+ int cat = ((IsNumeric)itm.getAttribute(ItmResource.ITM_CATEGORY)).getValue();
+ int abilType = -1;
+ int numAbil = ((IsNumeric)itm.getAttribute(ItmResource.ITM_NUM_ABILITIES)).getValue();
+ abilityIndex = Math.max(0, Math.min(numAbil - 1, abilityIndex));
+ if (abilityIndex < numAbil) {
+ List list = itm.getFields(Ability.class);
+ if (list != null && abilityIndex < list.size() && list.get(abilityIndex) instanceof Ability) {
+ Ability abil = (Ability)list.get(abilityIndex);
+ abilType = ((IsNumeric)abil.getAttribute(Ability.ABILITY_TYPE)).getValue();
+ }
+ }
+
+ switch (cat) {
+ case 15: // Bows
+ retVal = AttackType.BOW;
+ break;
+ case 27: // Crossbows
+ retVal = AttackType.CROSSBOW;
+ break;
+ case 18: // Slings
+ retVal = AttackType.SLING;
+ break;
+ default:
+ if (abilType == 1) { // melee
+ if (isTwoHanded) {
+ retVal = AttackType.TWO_HANDED;
+ } else {
+ retVal = (preferTwoWeapon) ? AttackType.TWO_WEAPON : AttackType.ONE_HANDED;
+ }
+ } else { // assume ranged
+ retVal = AttackType.THROWING;
+ }
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Attempts to determine the correct height code.
+ * @return the "guessed" height code. Returns empty string if code could not be determined.
+ */
+ protected String guessHeightCode()
+ {
+ String retVal = "";
+ boolean isCharacter = (getAnimationType() == AnimationType.CHARACTER);
+ String c2 = isCharacter ? "Q" : "P";
+
+ // try resref naming scheme
+ String resref = getAnimationResref().toUpperCase(Locale.ENGLISH);
+ if (resref.length() >= 3 && Pattern.matches(".[DEGHIO][FM].?", resref)) {
+ char race = resref.charAt(1);
+ char gender = resref.charAt(2);
+ if (gender == 'M' || gender == 'F') {
+ switch (race) {
+ case 'H': // human
+ case 'O': // half-orc
+ if (isCharacter) {
+ retVal = "W" + c2 + ((gender == 'F') ? "N" : "L");
+ } else {
+ retVal = "W" + c2 + "L";
+ }
+ break;
+ case 'E': // elf/half-elf
+ retVal = "W" + c2 + "M";
+ break;
+ case 'D': // dwarf/gnome
+ case 'G': // gnome (?)
+ case 'I': // halfling
+ retVal = "W" + c2 + "S";
+ break;
+ }
+ }
+ }
+
+ // try associated CRE data
+ if (retVal.isEmpty()) {
+ CreResource cre = getCreResource();
+ if (cre != null) {
+ boolean isFemale = ((IsNumeric)cre.getAttribute(CreResource.CRE_GENDER)).getValue() == 2;
+ int race = ((IsNumeric)cre.getAttribute(CreResource.CRE_RACE)).getValue();
+ switch (race) {
+ case 1: // human
+ case 7: // half-orc
+ if (isCharacter) {
+ retVal = "W" + c2 + (isFemale ? "N" : "L");
+ } else {
+ retVal = "W" + c2 + "L";
+ }
+ break;
+ case 2: // elf
+ case 3: // half-elf
+ retVal = "W" + c2 + "M";
+ break;
+ case 4: // dwarf
+ case 5: // halfling
+ case 6: // gnome
+ retVal = "W" + c2 + "S";
+ break;
+ }
+ }
+ }
+
+ return retVal;
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setCanLieDown(section.getAsInteger(KEY_CAN_LIE_DOWN.getName(), 0) != 0);
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ setDetectedByInfravision(section.getAsInteger(KEY_DETECTED_BY_INFRAVISION.getName(), 0) != 0);
+ setDoubleBlit(section.getAsInteger(KEY_DOUBLE_BLIT.getName(), 0) != 0);
+ setMaxArmorCode(section.getAsInteger(KEY_ARMOR_MAX_CODE.getName(), 0));
+ setHelmetEquipped(section.getAsInteger(KEY_EQUIP_HELMET.getName(), 0) != 0);
+ setHeightCode(section.getAsString(KEY_HEIGHT_CODE.getName(), ""));
+ setHelmetHeightCode(section.getAsString(KEY_HEIGHT_CODE_HELMET.getName(), ""));
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/CharacterDecoder.java b/src/org/infinity/resource/cre/decoder/CharacterDecoder.java
new file mode 100644
index 000000000..987305f22
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/CharacterDecoder.java
@@ -0,0 +1,461 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.itm.ItmResource;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type 5000/6000 (character) animations.
+ * Available ranges: [5000,53ff], [5500,55ff], [6000,63ff], [6500,65ff]
+ */
+public class CharacterDecoder extends ArmoredBaseDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.CHARACTER;
+
+ public static final DecoderAttribute KEY_SPLIT_BAMS = DecoderAttribute.with("split_bams", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_HEIGHT_CODE_SHIELD = DecoderAttribute.with("height_code_shield", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_RESREF_PAPERDOLL = DecoderAttribute.with("resref_paperdoll", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_RESREF_ARMOR_BASE = DecoderAttribute.with("resref_armor_base", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_RESREF_ARMOR_SPECIFIC = DecoderAttribute.with("resref_armor_specific", DecoderAttribute.DataType.STRING);
+
+ /** Assigns BAM suffix and cycle index to a specific animation sequence (unsplit version). */
+ private static final HashMap> suffixMapUnsplit =
+ new HashMap>() {{
+ put(Sequence.ATTACK_SLASH_1H, Couple.with("A1", 0));
+ put(Sequence.ATTACK_SLASH_2H, Couple.with("A2", 0));
+ put(Sequence.ATTACK_BACKSLASH_1H, Couple.with("A3", 0));
+ put(Sequence.ATTACK_BACKSLASH_2H, Couple.with("A4", 0));
+ put(Sequence.ATTACK_JAB_1H, Couple.with("A5", 0));
+ put(Sequence.ATTACK_JAB_2H, Couple.with("A6", 0));
+ put(Sequence.ATTACK_2WEAPONS1, Couple.with("A7", 0));
+ put(Sequence.ATTACK_OVERHEAD, Couple.with("A8", 0));
+ put(Sequence.ATTACK_2WEAPONS2, Couple.with("A9", 0));
+ put(Sequence.ATTACK_BOW, Couple.with("SA", 0));
+ put(Sequence.ATTACK_SLING, Couple.with("SS", 0));
+ put(Sequence.ATTACK_CROSSBOW, Couple.with("SX", 0));
+ put(Sequence.SPELL, Couple.with("CA", 0));
+ put(Sequence.SPELL1, get(Sequence.SPELL));
+ put(Sequence.SPELL2, Couple.with("CA", 18));
+ put(Sequence.SPELL3, Couple.with("CA", 36));
+ put(Sequence.SPELL4, Couple.with("CA", 54));
+ put(Sequence.CAST, Couple.with("CA", 9));
+ put(Sequence.CAST1, get(Sequence.CAST));
+ put(Sequence.CAST2, Couple.with("CA", 27));
+ put(Sequence.CAST3, Couple.with("CA", 45));
+ put(Sequence.CAST4, Couple.with("CA", 63));
+ put(Sequence.WALK, Couple.with("G1", 0));
+ put(Sequence.STANCE, Couple.with("G1", 9));
+ put(Sequence.STANCE2, Couple.with("G1", 27));
+ put(Sequence.STAND, Couple.with("G1", 18));
+ put(Sequence.STAND2, Couple.with("G1", 63));
+ put(Sequence.STAND3, Couple.with("G1", 72));
+ put(Sequence.GET_HIT, Couple.with("G1", 36));
+ put(Sequence.DIE, Couple.with("G1", 45));
+ put(Sequence.TWITCH, Couple.with("G1", 54));
+ put(Sequence.SLEEP, Couple.with("G1", 81));
+ put(Sequence.GET_UP, Couple.with("!G1", 81));
+ put(Sequence.SLEEP2, Couple.with("G1", 90));
+ put(Sequence.GET_UP2, Couple.with("!G1", 90));
+ }};
+
+ /** Assigns BAM suffix and cycle index to a specific animation sequence (split version). */
+ private static final HashMap> suffixMapSplit =
+ new HashMap>() {{
+ put(Sequence.ATTACK_SLASH_1H, Couple.with("A1", 0));
+ put(Sequence.ATTACK_SLASH_2H, Couple.with("A2", 0));
+ put(Sequence.ATTACK_BACKSLASH_1H, Couple.with("A3", 0));
+ put(Sequence.ATTACK_BACKSLASH_2H, Couple.with("A4", 0));
+ put(Sequence.ATTACK_JAB_1H, Couple.with("A5", 0));
+ put(Sequence.ATTACK_JAB_2H, Couple.with("A6", 0));
+ put(Sequence.ATTACK_2WEAPONS1, Couple.with("A7", 0));
+ put(Sequence.ATTACK_OVERHEAD, Couple.with("A8", 0));
+ put(Sequence.ATTACK_2WEAPONS2, Couple.with("A9", 0));
+ put(Sequence.ATTACK_BOW, Couple.with("SA", 0));
+ put(Sequence.ATTACK_SLING, Couple.with("SS", 0));
+ put(Sequence.ATTACK_CROSSBOW, Couple.with("SX", 0));
+ put(Sequence.SPELL, Couple.with("CA", 0));
+ put(Sequence.SPELL1, get(Sequence.SPELL));
+ put(Sequence.SPELL2, Couple.with("CA", 18));
+ put(Sequence.SPELL3, Couple.with("CA", 36));
+ put(Sequence.SPELL4, Couple.with("CA", 54));
+ put(Sequence.CAST, Couple.with("CA", 9));
+ put(Sequence.CAST1, get(Sequence.CAST));
+ put(Sequence.CAST2, Couple.with("CA", 27));
+ put(Sequence.CAST3, Couple.with("CA", 45));
+ put(Sequence.CAST4, Couple.with("CA", 63));
+ put(Sequence.WALK, Couple.with("G11", 0));
+ put(Sequence.STANCE, Couple.with("G1", 9));
+ put(Sequence.STANCE2, Couple.with("G13", 27));
+ put(Sequence.STAND, Couple.with("G12", 18));
+ put(Sequence.STAND2, Couple.with("G17", 63));
+ put(Sequence.STAND3, Couple.with("G18", 72));
+ put(Sequence.GET_HIT, Couple.with("G15", 36));
+ put(Sequence.DIE, Couple.with("G15", 45));
+ put(Sequence.TWITCH, Couple.with("G16", 54));
+ put(Sequence.SLEEP, Couple.with("G19", 81));
+ put(Sequence.GET_UP, Couple.with("!G19", 81));
+ put(Sequence.SLEEP2, Couple.with("G19", 90));
+ put(Sequence.GET_UP2, Couple.with("!G19", 90));
+ }};
+
+ /** Set of invalid attack type / animation sequence combinations. */
+ private static final EnumMap> forbiddenSequences =
+ new EnumMap>(AttackType.class) {{
+ put(AttackType.ONE_HANDED, EnumSet.of(Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_2WEAPONS1, Sequence.ATTACK_2WEAPONS2, Sequence.ATTACK_OVERHEAD,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_SLING, Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.TWO_HANDED, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_2WEAPONS1, Sequence.ATTACK_2WEAPONS2, Sequence.ATTACK_OVERHEAD,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_SLING, Sequence.ATTACK_CROSSBOW, Sequence.STANCE));
+ put(AttackType.TWO_WEAPON, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_OVERHEAD, Sequence.ATTACK_BOW, Sequence.ATTACK_SLING, Sequence.ATTACK_CROSSBOW,
+ Sequence.STANCE2));
+ put(AttackType.THROWING, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_2WEAPONS1, Sequence.ATTACK_2WEAPONS2, Sequence.ATTACK_BOW,
+ Sequence.ATTACK_SLING, Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.BOW, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_2WEAPONS1, Sequence.ATTACK_2WEAPONS2, Sequence.ATTACK_OVERHEAD,
+ Sequence.ATTACK_SLING, Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.SLING, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_2WEAPONS1, Sequence.ATTACK_2WEAPONS2, Sequence.ATTACK_OVERHEAD,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.CROSSBOW, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_2WEAPONS1, Sequence.ATTACK_2WEAPONS2, Sequence.ATTACK_OVERHEAD,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_SLING, Sequence.STANCE2));
+ }};
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int equipHelmet = SpriteTables.valueToInt(data, SpriteTables.COLUMN_HELMET, 0);
+ int splitBams = SpriteTables.valueToInt(data, SpriteTables.COLUMN_SPLIT, 0);
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+ String heightCode = SpriteTables.valueToString(data, SpriteTables.COLUMN_HEIGHT, "");
+ String heightCodeHelmet = heightCode;
+ String heightCodeShield = SpriteTables.valueToString(data, SpriteTables.COLUMN_HEIGHT_SHIELD, "");
+ String resrefSpecific = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF2, "");
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[character]");
+ lines.add("equip_helmet=" + equipHelmet);
+ lines.add("split_bams=" + splitBams);
+ lines.add("false_color=" + falseColor);
+ lines.add("resref=" + resref);
+ if (!heightCode.isEmpty()) {
+ lines.add("height_code=" + heightCode);
+ }
+ if (!heightCodeHelmet.isEmpty()) {
+ lines.add("height_code_helmet=" + heightCodeHelmet);
+ }
+ if (!heightCodeShield.isEmpty()) {
+ lines.add("height_code_shield=" + heightCodeShield);
+ }
+ lines.add("resref_armor_base=" + resref.charAt(resref.length() - 1));
+ if (!resrefSpecific.isEmpty()) {
+ lines.add("resref_armor_specific=" + resrefSpecific.charAt(resrefSpecific.length() - 1));
+ }
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public CharacterDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public CharacterDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns the correct sequence map for the current settings. */
+ private HashMap> getSuffixMap()
+ {
+ return isSplittedBams() ? suffixMapSplit : suffixMapUnsplit;
+ }
+
+ /** Returns whether animations are spread over various subfiles. */
+ public boolean isSplittedBams() { return getAttribute(KEY_SPLIT_BAMS); }
+ protected void setSplittedBams(boolean b) { setAttribute(KEY_SPLIT_BAMS, b); }
+
+ /** Returns the height code prefix for shield overlay sprites. Falls back to generic height code if needed. */
+ public String getShieldHeightCode()
+ {
+ String retVal = getAttribute(KEY_HEIGHT_CODE_SHIELD);
+ if (retVal.isEmpty()) {
+ retVal = getHeightCode();
+ }
+ return retVal;
+ }
+
+ protected void setShieldHeightCode(String s) { setAttribute(KEY_HEIGHT_CODE_SHIELD, s); }
+
+ /** Returns the paperdoll resref. */
+ public String getPaperdollResref() { return getAttribute(KEY_RESREF_PAPERDOLL); }
+ protected void setPaperdollResref(String s) { setAttribute(KEY_RESREF_PAPERDOLL, s); }
+
+ /**
+ * Returns the animation resref for lesser armor types.
+ * Returns the same value as {@link #getAnimationResref()} if no base armor code is available.
+ */
+ public String getArmorBaseResref() { return getAttribute(KEY_RESREF_ARMOR_BASE); }
+ protected void setArmorBaseResref(String s)
+ {
+ if (s.isEmpty()) {
+ s = getAnimationResref();
+ } else {
+ s = getAnimationResref().substring(0, 3) + s.substring(0, 1);
+ }
+ setAttribute(KEY_RESREF_ARMOR_BASE, s);
+ }
+
+ /**
+ * Returns the animation resref for greater armor types.
+ * Returns the same value as {@link #getAnimationResref()} if no specific armor code is available.
+ */
+ public String getArmorSpecificResref() { return getAttribute(KEY_RESREF_ARMOR_SPECIFIC); }
+ protected void setArmorSpecificResref(String s)
+ {
+ if (s.isEmpty()) {
+ s = getAnimationResref();
+ } else {
+ s = getAnimationResref().substring(0, 3) + s.substring(0, 1);
+ }
+ setAttribute(KEY_RESREF_ARMOR_SPECIFIC, s);
+ }
+
+ /**
+ * Sets the maximum armor code value uses as suffix in animation filenames.
+ * Specify -1 to detect value automatically.
+ */
+ @Override
+ protected void setMaxArmorCode(int v)
+ {
+ if (v < 0) {
+ // autodetection: requires fully initialized resref definitions
+ for (int i = 1; i < 10 && v < 0; i++) {
+ String resref = getArmorSpecificResref();
+ if (!resref.isEmpty() && ResourceFactory.resourceExists(resref + i + "G1.BAM")) {
+ v = i;
+ }
+ }
+ }
+ super.setMaxArmorCode(v);
+ }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ ArrayList retVal = null;
+ String resref1 = getAnimationResref();
+ String resref2 = getArmorSpecificResref();
+
+ if (essential) {
+ HashSet files = new HashSet<>();
+ for (final HashMap.Entry> entry : getSuffixMap().entrySet()) {
+ String suffix = SegmentDef.fixBehaviorSuffix(entry.getValue().getValue0());
+ if (suffix.startsWith("G")) {
+ for (int i = 1; i <= getMaxArmorCode(); i++) {
+ String resref = (i < getMaxArmorCode()) ? resref1 : resref2;
+ files.add(resref + i + suffix + ".BAM");
+ }
+ }
+ }
+ retVal = new ArrayList<>(Arrays.asList(files.toArray(new String[files.size()])));
+ } else {
+ // collecting suffixes
+ HashSet actionSet = new HashSet<>();
+ for (final HashMap.Entry> entry : getSuffixMap().entrySet()) {
+ String suffix = SegmentDef.fixBehaviorSuffix(entry.getValue().getValue0());
+ actionSet.add(suffix);
+ }
+
+ // generating file list
+ retVal = new ArrayList() {{
+ for (int i = 1; i <= getMaxArmorCode(); i++) {
+ String resref = (i < getMaxArmorCode()) ? resref1 : resref2;
+ for (final String a : actionSet) {
+ add(resref + i + a + ".BAM");
+ }
+ }
+ }};
+ }
+
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ super.init();
+ IniMapSection section = getSpecificIniSection();
+ setSplittedBams(section.getAsInteger(KEY_SPLIT_BAMS.getName(), 0) != 0);
+ setShieldHeightCode(section.getAsString(KEY_HEIGHT_CODE_SHIELD.getName(), ""));
+ setPaperdollResref(section.getAsString(KEY_RESREF_PAPERDOLL.getName(), ""));
+ setArmorBaseResref(section.getAsString(KEY_RESREF_ARMOR_BASE.getName(), ""));
+ setArmorSpecificResref(section.getAsString(KEY_RESREF_ARMOR_SPECIFIC.getName(), ""));
+ if (getMaxArmorCode() == 0) {
+ setMaxArmorCode(-1);
+ }
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+
+ if (!getSuffixMap().containsKey(seq)) {
+ return retVal;
+ }
+
+ // getting armor level
+ int armorCode = getArmorCode();
+ if (armorCode > getMaxArmorCode()) {
+ return retVal;
+ }
+
+ // preparing shield slot data
+ boolean isLefthandedWeapon = false;
+ String prefixShield = getShieldHeightCode();
+ String codeShield = "";
+ if (!prefixShield.isEmpty()) {
+ ItmResource itm = SpriteUtils.getEquippedShield(getCreResource());
+ codeShield = SpriteUtils.getItemAppearance(itm).trim();
+ isLefthandedWeapon = !codeShield.isEmpty() && SpriteUtils.isLeftHandedWeapon(itm);
+ }
+
+ // getting attack type
+ ItmResource itmWeapon = SpriteUtils.getEquippedWeapon(getCreResource());
+ int itmAbility = SpriteUtils.getEquippedWeaponAbility(getCreResource());
+ AttackType attackType = getAttackType(itmWeapon, itmAbility, isLefthandedWeapon);
+
+ EnumSet sequences = forbiddenSequences.get(attackType);
+ if (sequences != null && sequences.contains(seq)) {
+ // sequence not allowed for selected weapon
+ return retVal;
+ }
+
+ ArrayList> resrefList = new ArrayList<>();
+
+ // adding creature resref
+ String creSuffix = getSuffixMap().get(seq).getValue0();
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(creSuffix);
+ creSuffix = SegmentDef.fixBehaviorSuffix(creSuffix);
+ String creResref = (armorCode > 1) ? getArmorSpecificResref() : getArmorBaseResref();
+ if (!ResourceFactory.resourceExists(creResref + armorCode + creSuffix + ".BAM")) {
+ creResref = getArmorBaseResref();
+ if (!ResourceFactory.resourceExists(creResref + armorCode + creSuffix + ".BAM")) {
+ creResref = getAnimationResref();
+ }
+ }
+ creResref += armorCode;
+ resrefList.add(Couple.with(creResref, SegmentDef.SpriteType.AVATAR));
+
+ String prefix;
+ // adding helmet overlay
+ if (isHelmetEquipped()) {
+ prefix = getHelmetHeightCode();
+ if (!prefix.isEmpty()) {
+ String code = SpriteUtils.getItemAppearance(SpriteUtils.getEquippedHelmet(getCreResource())).trim();
+ if (code.length() == 2) {
+ resrefList.add(Couple.with(prefix + code, SegmentDef.SpriteType.HELMET));
+ }
+ }
+ }
+
+ // adding shield overlay
+ if (!prefixShield.isEmpty() && !codeShield.isEmpty()) {
+ resrefList.add(Couple.with(prefixShield + codeShield, SegmentDef.SpriteType.SHIELD));
+ }
+
+ // adding weapon overlay
+ prefix = getHeightCode();
+ if (!prefix.isEmpty()) {
+ String code = SpriteUtils.getItemAppearance(itmWeapon).trim();
+ if (code.length() == 2) {
+ resrefList.add(Couple.with(prefix + code, SegmentDef.SpriteType.WEAPON));
+ }
+ }
+
+ retVal = new SeqDef(seq);
+ for (final Couple data: resrefList) {
+ // getting BAM suffix and cycle index
+ prefix = data.getValue0();
+ SegmentDef.SpriteType spriteType = data.getValue1();
+ HashMap> curSuffixMap = (spriteType == SegmentDef.SpriteType.AVATAR) ? getSuffixMap() : suffixMapUnsplit;
+ String suffix = SegmentDef.fixBehaviorSuffix(curSuffixMap.get(seq).getValue0());
+ int cycleIdx = curSuffixMap.get(seq).getValue1().intValue();
+
+ // enabling left-handed weapon overlay if available
+ if (spriteType == SegmentDef.SpriteType.SHIELD && isLefthandedWeapon) {
+ if (ResourceFactory.resourceExists(prefix + "O" + suffix + ".BAM")) {
+ prefix += "O";
+ }
+ }
+
+ // defining sequences
+ ResourceEntry entry = ResourceFactory.getResourceEntry(prefix + suffix + ".BAM");
+ if (SpriteUtils.bamCyclesExist(entry, cycleIdx, SeqDef.DIR_FULL_W.length)) {
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_FULL_W, false, entry, cycleIdx, spriteType, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ tmp = SeqDef.createSequence(seq, SeqDef.DIR_FULL_E, true, entry, cycleIdx + 1, spriteType, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+
+ if (retVal.isEmpty()) {
+ retVal = null;
+ }
+
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/CharacterOldDecoder.java b/src/org/infinity/resource/cre/decoder/CharacterOldDecoder.java
new file mode 100644
index 000000000..dd6e054d0
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/CharacterOldDecoder.java
@@ -0,0 +1,385 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.CycleDef;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.itm.ItmResource;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type 5000/6000 (character_old) animations.
+ * Available ranges: [5400,54ff], [5600,5fff], [6400,64ff], [6600,6fff]
+ */
+public class CharacterOldDecoder extends ArmoredBaseDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.CHARACTER_OLD;
+
+ public static final DecoderAttribute KEY_HIDE_WEAPONS = DecoderAttribute.with("hide_weapons", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_SHADOW = DecoderAttribute.with("shadow", DecoderAttribute.DataType.STRING);
+
+ /** Assigns BAM suffix and cycle index to a specific animation sequence. */
+ private static final HashMap> suffixMap =
+ new HashMap>() {{
+ put(Sequence.ATTACK_SLASH_1H, Couple.with("A1", 0));
+ put(Sequence.ATTACK_SLASH_2H, Couple.with("A2", 0));
+ put(Sequence.ATTACK_BACKSLASH_1H, Couple.with("A3", 0));
+ put(Sequence.ATTACK_BACKSLASH_2H, Couple.with("A4", 0));
+ put(Sequence.ATTACK_JAB_1H, Couple.with("A5", 0));
+ put(Sequence.ATTACK_JAB_2H, Couple.with("A6", 0));
+ put(Sequence.ATTACK_BOW, Couple.with("SA", 0));
+ put(Sequence.ATTACK_CROSSBOW, Couple.with("SX", 0));
+ put(Sequence.SPELL, Couple.with("CA", 0));
+ put(Sequence.SPELL1, get(Sequence.SPELL));
+ put(Sequence.SPELL2, Couple.with("CA", 16));
+ put(Sequence.SPELL3, Couple.with("CA", 32));
+ put(Sequence.SPELL4, Couple.with("CA", 48));
+ put(Sequence.CAST, Couple.with("CA", 8));
+ put(Sequence.CAST1, get(Sequence.CAST));
+ put(Sequence.CAST2, Couple.with("CA", 24));
+ put(Sequence.CAST3, Couple.with("CA", 40));
+ put(Sequence.CAST4, Couple.with("CA", 56));
+ put(Sequence.WALK, Couple.with("G1", 0));
+ put(Sequence.STANCE, Couple.with("G1", 8));
+ put(Sequence.STANCE2, Couple.with("G1", 24));
+ put(Sequence.STAND, Couple.with("G1", 16));
+ put(Sequence.STAND2, Couple.with("G1", 32));
+ put(Sequence.GET_HIT, Couple.with("G1", 40));
+ put(Sequence.DIE, Couple.with("G1", 48));
+ put(Sequence.SLEEP, get(Sequence.DIE));
+ put(Sequence.GET_UP, Couple.with("!G1", 48));
+ put(Sequence.TWITCH, Couple.with("G1", 56));
+ }};
+
+ /** BAM suffix and cycle index for extended walk directions. */
+ private static Couple walkExtra = Couple.with("W2", 0);
+
+ /** Set of invalid attack type / animation sequence combinations. */
+ private static final EnumMap> forbiddenSequences =
+ new EnumMap>(AttackType.class) {{
+ put(AttackType.ONE_HANDED, EnumSet.of(Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.TWO_HANDED, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_CROSSBOW, Sequence.STANCE));
+ put(AttackType.TWO_WEAPON, EnumSet.allOf(Sequence.class));
+ put(AttackType.THROWING, EnumSet.of(Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_BOW, Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.BOW, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_CROSSBOW, Sequence.STANCE2));
+ put(AttackType.SLING, get(AttackType.THROWING));
+ put(AttackType.CROSSBOW, EnumSet.of(Sequence.ATTACK_SLASH_1H, Sequence.ATTACK_BACKSLASH_1H, Sequence.ATTACK_JAB_1H,
+ Sequence.ATTACK_SLASH_2H, Sequence.ATTACK_BACKSLASH_2H, Sequence.ATTACK_JAB_2H,
+ Sequence.ATTACK_BOW, Sequence.STANCE2));
+ }};
+
+ // the default shadow of the animation
+ private static final String SHADOW_RESREF = "CSHD";
+ // special shadow for (armored) Sarevok
+ private static final String SHADOW_RESREF_SAREVOK = "SSHD";
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int equipHelmet = SpriteTables.valueToInt(data, SpriteTables.COLUMN_HELMET, 0);
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+ int hideWeapons = SpriteTables.valueToInt(data, SpriteTables.COLUMN_WEAPON, 1) != 0 ? 0 : 1;
+ String heightCode = SpriteTables.valueToString(data, SpriteTables.COLUMN_HEIGHT, "");
+ String heightCodeHelmet = heightCode;
+ String shadow = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF2, "");
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[character_old]");
+ lines.add("equip_helmet=" + equipHelmet);
+ lines.add("false_color=" + falseColor);
+ lines.add("hide_weapons=" + hideWeapons);
+ lines.add("resref=" + resref);
+ if (!heightCode.isEmpty()) {
+ lines.add("height_code=" + heightCode);
+ }
+ if (!heightCodeHelmet.isEmpty()) {
+ lines.add("height_code_helmet=" + heightCodeHelmet);
+ }
+ if (!shadow.isEmpty()) {
+ lines.add("shadow=" + shadow);
+ }
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public CharacterOldDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public CharacterOldDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns whether weapon animation overlays are suppressed. */
+ public boolean isWeaponsHidden() { return getAttribute(KEY_HIDE_WEAPONS); }
+ protected void setWeaponsHidden(boolean b) { setAttribute(KEY_HIDE_WEAPONS, b); }
+
+ /** Returns a separate shadow sprite resref. */
+ public String getShadowResref() { return getAttribute(KEY_SHADOW); }
+ protected void setShadowResref(String s)
+ {
+ String shadow;
+ // taking care of harcoded shadows
+ switch (getAnimationId()) {
+ case 0x6400: // Drizzt
+ shadow = SHADOW_RESREF;
+ break;
+ case 0x6404: // Sarevok
+ shadow = SHADOW_RESREF_SAREVOK;
+ break;
+ default:
+ shadow = !s.isEmpty() ? s : SHADOW_RESREF;
+ }
+ setAttribute(KEY_SHADOW, shadow);
+ }
+
+ /**
+ * Sets the maximum armor code value uses as suffix in animation filenames.
+ * Specify -1 to detect value automatically.
+ */
+ @Override
+ protected void setMaxArmorCode(int v)
+ {
+ if (v < 0) {
+ // autodetection
+ for (int i = 1; i < 10 && v < 0; i++) {
+ String resref = getAnimationResref();
+ if (!resref.isEmpty() && !ResourceFactory.resourceExists(resref + i + "G1.BAM")) {
+ v = i - 1;
+ }
+ }
+ }
+ super.setMaxArmorCode(v);
+ }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ ArrayList retVal = null;
+ String resref = getAnimationResref();
+
+ if (essential) {
+ HashSet files = new HashSet<>();
+ for (final HashMap.Entry> entry : suffixMap.entrySet()) {
+ String suffix = SegmentDef.fixBehaviorSuffix(entry.getValue().getValue0());
+ if (suffix.startsWith("G")) {
+ for (int i = 1; i <= getMaxArmorCode(); i++) {
+ files.add(resref + i + suffix + ".BAM");
+ }
+ }
+ }
+ retVal = new ArrayList<>(Arrays.asList(files.toArray(new String[files.size()])));
+ } else {
+ // collecting suffixes
+ HashSet actionSet = new HashSet<>();
+ for (final HashMap.Entry> entry : suffixMap.entrySet()) {
+ String suffix = SegmentDef.fixBehaviorSuffix(entry.getValue().getValue0());
+ actionSet.add(suffix);
+ }
+ actionSet.add(walkExtra.getValue0());
+
+ // generating file list
+ retVal = new ArrayList() {{
+ for (int i = 1; i <= getMaxArmorCode(); i++) {
+ for (final String a : actionSet) {
+ add(resref + i + a + ".BAM");
+ add(resref + i + a + "E.BAM");
+ }
+ }
+ }};
+ }
+
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ super.init();
+ IniMapSection section = getSpecificIniSection();
+ setWeaponsHidden(section.getAsInteger(KEY_HIDE_WEAPONS.getName(), 0) != 0);
+ setShadowResref(section.getAsString(KEY_SHADOW.getName(), ""));
+ if (getMaxArmorCode() == 0) {
+ setMaxArmorCode(-1);
+ }
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+
+ if (!suffixMap.containsKey(seq)) {
+ return retVal;
+ }
+
+ // getting armor level
+ int armorCode = getArmorCode();
+ if (armorCode > getMaxArmorCode()) {
+ return retVal;
+ }
+
+ // getting attack type
+ ItmResource itmWeapon = SpriteUtils.getEquippedWeapon(getCreResource());
+ int itmAbility = SpriteUtils.getEquippedWeaponAbility(getCreResource());
+ AttackType attackType = getAttackType(itmWeapon, itmAbility, false);
+
+ EnumSet sequences = forbiddenSequences.get(attackType);
+ if (sequences != null && sequences.contains(seq)) {
+ // sequence not allowed for selected weapon
+ return retVal;
+ }
+
+ ArrayList> resrefList = new ArrayList<>();
+
+ // getting BAM suffix and cycle index
+ String suffix = suffixMap.get(seq).getValue0();
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(suffix);
+ suffix = SegmentDef.fixBehaviorSuffix(suffix);
+ int cycleIdx = suffixMap.get(seq).getValue1().intValue();
+
+ // adding creature shadow
+ if (!getShadowResref().isEmpty()) {
+ resrefList.add(Couple.with(getShadowResref(), SegmentDef.SpriteType.AVATAR));
+ }
+
+ // adding creature sprite
+ resrefList.add(Couple.with(getAnimationResref() + armorCode, SegmentDef.SpriteType.AVATAR));
+
+ // adding helmet overlay
+ if (isHelmetEquipped()) {
+ String prefix = getHelmetHeightCode();
+ if (!prefix.isEmpty()) {
+ String code = SpriteUtils.getItemAppearance(SpriteUtils.getEquippedHelmet(getCreResource()));
+ if (code.length() == 2) {
+ resrefList.add(Couple.with(prefix + code, SegmentDef.SpriteType.HELMET));
+ }
+ }
+ }
+
+ if (!isWeaponsHidden()) {
+ // adding shield overlay
+ String prefix = getHeightCode();
+ if (!prefix.isEmpty()) {
+ ItmResource itm = SpriteUtils.getEquippedShield(getCreResource());
+ String code = SpriteUtils.getItemAppearance(itm);
+ if (!code.isEmpty()) {
+ resrefList.add(Couple.with(prefix + code, SegmentDef.SpriteType.SHIELD));
+ }
+ }
+
+ // adding weapon overlay
+ prefix = getHeightCode();
+ if (!prefix.isEmpty()) {
+ String code = SpriteUtils.getItemAppearance(itmWeapon);
+ if (code.length() == 2) {
+ if (ResourceFactory.resourceExists(prefix + code + suffix + ".BAM")) {
+ resrefList.add(Couple.with(prefix + code, SegmentDef.SpriteType.WEAPON));
+ } else {
+ // weapon animation is crucial
+ return retVal;
+ }
+ }
+ }
+ }
+
+ retVal = new SeqDef(seq);
+ for (final Couple data : resrefList) {
+ String prefix = data.getValue0();
+ SegmentDef.SpriteType spriteType = data.getValue1();
+ // defining sequences
+ ResourceEntry entry = ResourceFactory.getResourceEntry(prefix + suffix + ".BAM");
+ ResourceEntry entryE = ResourceFactory.getResourceEntry(prefix + suffix + "E.BAM");
+ if (entry != null) {
+ if (seq == Sequence.WALK) {
+ // special: uses full set of directions spread over two BAM files
+ String suffix2 = walkExtra.getValue0();
+ int cycleIdx2 = walkExtra.getValue1().intValue();
+ ResourceEntry entry2 = ResourceFactory.getResourceEntry(prefix + suffix2 + ".BAM");
+ ResourceEntry entry2E = ResourceFactory.getResourceEntry(prefix + suffix2 + "E.BAM");
+ if (SpriteUtils.bamCyclesExist(entry, cycleIdx, SeqDef.DIR_REDUCED_W.length) &&
+ SpriteUtils.bamCyclesExist(entry2, cycleIdx2, SeqDef.DIR_REDUCED_W.length) &&
+ SpriteUtils.bamCyclesExist(entryE, cycleIdx + SeqDef.DIR_REDUCED_W.length, SeqDef.DIR_REDUCED_E.length) &&
+ SpriteUtils.bamCyclesExist(entry2E, cycleIdx2 + SeqDef.DIR_REDUCED_W.length, SeqDef.DIR_REDUCED_E.length)) {
+ // defining western directions
+ Direction[] dirX = new Direction[] { Direction.SSW, Direction.WSW, Direction.WNW, Direction.NNW, Direction.NNE };
+ for (int i = 0; i < SeqDef.DIR_REDUCED_W.length; i++) {
+ retVal.addDirections(new DirDef(SeqDef.DIR_REDUCED_W[i], false, new CycleDef(entry, cycleIdx + i, spriteType, behavior)));
+ retVal.addDirections(new DirDef(dirX[i], false, new CycleDef(entry2, cycleIdx2 + i, spriteType, behavior)));
+ }
+ // defining eastern directions
+ dirX = new Direction[] { Direction.ENE, Direction.ESE, Direction.SSE, };
+ for (int i = 0; i < SeqDef.DIR_REDUCED_E.length; i++) {
+ retVal.addDirections(new DirDef(SeqDef.DIR_REDUCED_E[i], false, new CycleDef(entryE, cycleIdx + SeqDef.DIR_REDUCED_W.length + i, spriteType, behavior)));
+ retVal.addDirections(new DirDef(dirX[i], false, new CycleDef(entry2E, cycleIdx2 + SeqDef.DIR_REDUCED_W.length + i, spriteType, behavior)));
+ }
+ }
+ } else {
+ if (SpriteUtils.bamCyclesExist(entry, cycleIdx, SeqDef.DIR_REDUCED_W.length) &&
+ SpriteUtils.bamCyclesExist(entry, cycleIdx + SeqDef.DIR_REDUCED_W.length, SeqDef.DIR_REDUCED_E.length)) {
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_W, false, entry, cycleIdx, spriteType, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_E, false, entryE, cycleIdx + SeqDef.DIR_REDUCED_W.length, spriteType, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+ }
+ }
+
+ if (retVal.isEmpty()) {
+ retVal = null;
+ }
+
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/EffectDecoder.java b/src/org/infinity/resource/cre/decoder/EffectDecoder.java
new file mode 100644
index 000000000..84aec65da
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/EffectDecoder.java
@@ -0,0 +1,257 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.CycleDef;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type 0000 (effect) animations.
+ * Available ranges: [0000,0fff]
+ */
+public class EffectDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.EFFECT;
+
+ public static final DecoderAttribute KEY_SHADOW = DecoderAttribute.with("shadow", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_PALLETIZED = DecoderAttribute.with("palletized", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_RANDOM_RENDER = DecoderAttribute.with("random_render", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_DELTA_Z = DecoderAttribute.with("delta_z", DecoderAttribute.DataType.INT);
+ public static final DecoderAttribute KEY_ALT_PALETTE = DecoderAttribute.with("alt_palette", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_HEIGHT_CODE_SHIELD = DecoderAttribute.with("height_code_shield", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_HEIGHT_CODE_HELMET = DecoderAttribute.with("height_code_helmet", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_HEIGHT_CODE = DecoderAttribute.with("height_code", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_RESREF_PAPERDOLL = DecoderAttribute.with("resref_paperdoll", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_RESREF_ARMOR_BASE = DecoderAttribute.with("resref_armor_base", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_RESREF_ARMOR_SPECIFIC = DecoderAttribute.with("resref_armor_specific", DecoderAttribute.DataType.STRING);
+ // Note: these attribute are artificial to store hardcoded information
+ // The cycle to play back (if >= 0)
+ public static final DecoderAttribute KEY_CYCLE = DecoderAttribute.with("cycle", DecoderAttribute.DataType.INT);
+ // A secondary resref to consider if random_render == 1
+ public static final DecoderAttribute KEY_RESREF2 = DecoderAttribute.with("resref2", DecoderAttribute.DataType.STRING);
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ String shadow = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF2, "");
+ int translucent = SpriteTables.valueToInt(data, SpriteTables.COLUMN_TRANSLUCENT, 0);
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+ int random = SpriteTables.valueToInt(data, SpriteTables.COLUMN_SPLIT, 0);
+ int cycle = SpriteTables.valueToInt(data, SpriteTables.COLUMN_HELMET, -1);
+ String altPalette = SpriteTables.valueToString(data, SpriteTables.COLUMN_PALETTE2, "");
+ String resref2 = SpriteTables.valueToString(data, SpriteTables.COLUMN_HEIGHT, "");
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[effect]");
+ lines.add("resref=" + resref);
+ if (!shadow.isEmpty()) {
+ lines.add("shadow=" + shadow);
+ }
+ lines.add("translucent=" + translucent);
+ lines.add("false_color=" + falseColor);
+ lines.add("random_render=" + random);
+ if (cycle >= 0) {
+ lines.add("cycle=" + cycle);
+ }
+ if (!altPalette.isEmpty()) {
+ lines.add("alt_palette=" + altPalette);
+ }
+ if (!resref2.isEmpty()) {
+ lines.add("resref2=" + resref2);
+ }
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public EffectDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public EffectDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns a separate shadow sprite resref. */
+ public String getShadowResref() { return getAttribute(KEY_SHADOW); }
+ protected void setShadowResref(String s) { setAttribute(KEY_SHADOW, s); }
+
+ /** Returns a secondary sprite resref to consider if RenderRandom() is set. */
+ public String getSecondaryResref() { return getAttribute(KEY_RESREF2); }
+ protected void setSecondaryResref(String s) { setAttribute(KEY_RESREF2, s); }
+
+ /** Returns a replacement palette resref (BMP). */
+ public String getAltPalette() { return getAttribute(KEY_ALT_PALETTE); }
+ protected void setAltPalette(String s) { setAttribute(KEY_ALT_PALETTE, s); }
+
+ /** Returns the height code prefix for shield overlay sprites. */
+ public String getShieldHeightCode() { return getAttribute(KEY_HEIGHT_CODE_SHIELD); }
+ protected void setShieldHeightCode(String s) { setAttribute(KEY_HEIGHT_CODE_SHIELD, s); }
+
+ /** Returns the height code prefix for helmet overlay sprites. */
+ public String getHelmetHeightCode() { return getAttribute(KEY_HEIGHT_CODE_HELMET); }
+ protected void setHelmetHeightCode(String s) { setAttribute(KEY_HEIGHT_CODE_HELMET, s); }
+
+ /** Returns the creature animation height code prefix. */
+ public String getHeightCode() { return getAttribute(KEY_HEIGHT_CODE); }
+ protected void setHeightCode(String s) { setAttribute(KEY_HEIGHT_CODE, s); }
+
+ /** Returns the paperdoll resref. */
+ public String getPaperdollResref() { return getAttribute(KEY_RESREF_PAPERDOLL); }
+ protected void setPaperdollResref(String s) { setAttribute(KEY_RESREF_PAPERDOLL, s); }
+
+ /** Returns animation resref suffix (last letter) for lesser armor types. */
+ public String getArmorBaseResref() { return getAttribute(KEY_RESREF_ARMOR_BASE); }
+ protected void setArmorBaseResref(String s) { setAttribute(KEY_RESREF_ARMOR_BASE, s); }
+
+ /** Returns animation resref suffix (last letter) for greater armor types. */
+ public String getArmorSpecificResref() { return getAttribute(KEY_RESREF_ARMOR_SPECIFIC); }
+ protected void setArmorSpecificResref(String s) { setAttribute(KEY_RESREF_ARMOR_SPECIFIC, s); }
+
+ /** unused */
+ public boolean isPalettized() { return getAttribute(KEY_PALLETIZED); }
+ protected void setPalettized(boolean b) { setAttribute(KEY_PALLETIZED, b); }
+
+ /** Returns whether a randomly chosen animation cycle is drawn. */
+ public boolean isRenderRandom() { return getAttribute(KEY_RANDOM_RENDER); }
+ protected void setRenderRandom(boolean b) { setAttribute(KEY_RANDOM_RENDER, b); }
+
+ /** Returns the BAM cycle index to use. -1 indicates no specific BAM cycle. */
+ public int getCycle() { return getAttribute(KEY_CYCLE); }
+ protected void setCycle(int v) { setAttribute(KEY_CYCLE, v); }
+
+ /** ??? */
+ public int getDeltaZ() { return getAttribute(KEY_DELTA_Z); }
+ protected void setDeltaZ(int v) { setAttribute(KEY_DELTA_Z, v); }
+
+ @Override
+ public String getNewPalette()
+ {
+ String retVal = getAltPalette();
+ if (retVal == null || retVal.isEmpty()) {
+ retVal = super.getNewPalette();
+ }
+ return retVal;
+ }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ ArrayList retVal = new ArrayList<>();
+ retVal.add(getAnimationResref() + ".BAM");
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setShadowResref(section.getAsString(KEY_SHADOW.getName(), ""));
+ setPalettized(section.getAsInteger(KEY_PALLETIZED.getName(), 0) != 0);
+ setTranslucent(section.getAsInteger(KEY_TRANSLUCENT.getName(), 0) != 0);
+ setRenderRandom(section.getAsInteger(KEY_RANDOM_RENDER.getName(), 0) != 0);
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ setCycle(section.getAsInteger(KEY_CYCLE.getName(), -1));
+ setDeltaZ(section.getAsInteger(KEY_DELTA_Z.getName(), 0));
+ setAltPalette(section.getAsString(KEY_ALT_PALETTE.getName(), ""));
+ setShieldHeightCode(section.getAsString(KEY_HEIGHT_CODE_SHIELD.getName(), ""));
+ setHelmetHeightCode(section.getAsString(KEY_HEIGHT_CODE_HELMET.getName(), ""));
+ setHeightCode(section.getAsString(KEY_HEIGHT_CODE.getName(), ""));
+ setPaperdollResref(section.getAsString(KEY_RESREF_PAPERDOLL.getName(), ""));
+ setArmorBaseResref(section.getAsString(KEY_RESREF_ARMOR_BASE.getName(), ""));
+ setArmorSpecificResref(section.getAsString(KEY_RESREF_ARMOR_SPECIFIC.getName(), ""));
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+ if (seq != Sequence.STAND) {
+ return retVal;
+ }
+
+ ArrayList> creResList = new ArrayList<>();
+ if (!getShadowResref().isEmpty()) {
+ ResourceEntry shdEntry = ResourceFactory.getResourceEntry(getShadowResref() + ".BAM");
+ if (shdEntry != null) {
+ creResList.add(Couple.with(shdEntry, 0));
+ }
+ }
+
+ Random rnd = new Random();
+ String resref = getAnimationResref();
+ if (isRenderRandom()) {
+ if (!getSecondaryResref().isEmpty() && (Math.abs(rnd.nextInt()) % 3) == 0) {
+ resref = getSecondaryResref();
+ }
+ }
+
+ ResourceEntry resEntry = ResourceFactory.getResourceEntry(resref + ".BAM");
+ BamControl ctrl = SpriteUtils.loadBamController(resEntry);
+ if (ctrl != null) {
+ int cycle = 0;
+ if (isRenderRandom()) {
+ cycle = Math.abs(rnd.nextInt()) % ctrl.cycleCount();
+ } else if (getCycle() >= 0) {
+ cycle = getCycle();
+ }
+ creResList.add(Couple.with(resEntry, 0));
+
+ retVal = new SeqDef(seq);
+ for (final Couple data : creResList) {
+ resEntry = data.getValue0();
+ cycle = data.getValue1().intValue();
+ if (SpriteUtils.bamCyclesExist(resEntry, cycle, 1)) {
+ retVal.addDirections(new DirDef(Direction.S, false, new CycleDef(resEntry, cycle)));
+ }
+ }
+ }
+
+ if (retVal.isEmpty()) {
+ retVal = null;
+ }
+
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/FlyingDecoder.java b/src/org/infinity/resource/cre/decoder/FlyingDecoder.java
new file mode 100644
index 000000000..d8d2b0a85
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/FlyingDecoder.java
@@ -0,0 +1,116 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+
+/**
+ * Creature animation decoder for processing type D000 (flying) animations.
+ * Available ranges: [d000,dfff]
+ */
+public class FlyingDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.FLYING;
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[flying]");
+ lines.add("false_color=" + falseColor);
+ lines.add("resref=" + resref);
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public FlyingDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public FlyingDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ ArrayList retVal = new ArrayList<>();
+ retVal.add(getAnimationResref() + ".BAM");
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+
+ int cycleIndex = 0;
+ switch (seq) {
+ case STAND:
+ cycleIndex = 0;
+ break;
+ case WALK:
+ cycleIndex = 9;
+ break;
+ default:
+ return retVal;
+ }
+
+ ResourceEntry entry = ResourceFactory.getResourceEntry(getAnimationResref() + ".BAM");
+ if (SpriteUtils.bamCyclesExist(entry, cycleIndex, SeqDef.DIR_FULL_W.length)) {
+ retVal = SeqDef.createSequence(seq, SeqDef.DIR_FULL_W, false, entry, cycleIndex);
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_FULL_E, true, entry, cycleIndex + 1);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/MonsterAnkhegDecoder.java b/src/org/infinity/resource/cre/decoder/MonsterAnkhegDecoder.java
new file mode 100644
index 000000000..a34728a03
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/MonsterAnkhegDecoder.java
@@ -0,0 +1,178 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type 3000 (monster_ankheg) animations.
+ * Available ranges: [3000,3fff]
+ */
+public class MonsterAnkhegDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.MONSTER_ANKHEG;
+
+ public static final DecoderAttribute KEY_MIRROR = DecoderAttribute.with("mirror", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_EXTEND_DIRECTION = DecoderAttribute.with("extend_direction", DecoderAttribute.DataType.BOOLEAN);
+
+ private static final HashMap> suffixMap = new HashMap>() {{
+ // Note: int value indicates direction segment multiplier
+ put(Sequence.DIE, Couple.with("G1", 1));
+ put(Sequence.SLEEP, get(Sequence.DIE));
+ put(Sequence.GET_UP, Couple.with("!G1", 1));
+ put(Sequence.TWITCH, Couple.with("G1", 2));
+ put(Sequence.STAND_EMERGED, Couple.with("G1", 3));
+ put(Sequence.STAND_HIDDEN, Couple.with("G2", 0));
+ put(Sequence.EMERGE, Couple.with("G2", 1));
+ put(Sequence.HIDE, Couple.with("G2", 2));
+ put(Sequence.ATTACK, Couple.with("G3", 0));
+ put(Sequence.SPELL, Couple.with("G3", 1));
+ }};
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[monster_ankheg]");
+ lines.add("false_color=" + falseColor);
+ lines.add("resref=" + resref);
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public MonsterAnkhegDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public MonsterAnkhegDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns whether eastern directions are calculated. */
+ public boolean hasMirroredDirections() { return getAttribute(KEY_MIRROR); }
+ protected void setMirroredDirections(boolean b) { setAttribute(KEY_MIRROR, b); }
+
+ /** Returns whether the animation provides the full set of directions. */
+ public boolean hasExtendedDirections() { return getAttribute(KEY_EXTEND_DIRECTION); }
+ protected void setExtendedDirections(boolean b) { setAttribute(KEY_EXTEND_DIRECTION, b); }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ String resref = getAnimationResref();
+ ArrayList retVal = new ArrayList() {{
+ add(resref + "DG1.BAM");
+ if (!hasMirroredDirections()) { add(resref + "DG1E.BAM"); }
+ add(resref + "DG2.BAM");
+ if (!hasMirroredDirections()) { add(resref + "DG2E.BAM"); }
+ add(resref + "DG3.BAM");
+ if (!hasMirroredDirections()) { add(resref + "DG3E.BAM"); }
+ add(resref + "G1.BAM");
+ if (!hasMirroredDirections()) { add(resref + "G1E.BAM"); }
+ add(resref + "G2.BAM");
+ if (!hasMirroredDirections()) { add(resref + "G2E.BAM"); }
+ add(resref + "G3.BAM");
+ if (!hasMirroredDirections()) { add(resref + "G3E.BAM"); }
+ }};
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setMirroredDirections(section.getAsInteger(KEY_MIRROR.getName(), 0) != 0);
+ setExtendedDirections(section.getAsInteger(KEY_EXTEND_DIRECTION.getName(), 0) != 0);
+ setDetectedByInfravision(section.getAsInteger(KEY_DETECTED_BY_INFRAVISION.getName(), 0) != 0);
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+ String resref = getAnimationResref();
+
+ Direction[] dirWest = hasExtendedDirections() ? SeqDef.DIR_FULL_W : SeqDef.DIR_REDUCED_W;
+ Direction[] dirEast = hasExtendedDirections() ? SeqDef.DIR_FULL_E : SeqDef.DIR_REDUCED_E;
+ int seg = dirWest.length;
+ if (!hasMirroredDirections()) {
+ seg += dirEast.length;
+ }
+
+ String suffixE = hasMirroredDirections() ? "" : "E";
+ int eastOfs = hasMirroredDirections() ? 1 : dirWest.length;
+ Couple data = suffixMap.get(seq);
+ if (data == null) {
+ return retVal;
+ }
+
+ retVal = new SeqDef(seq);
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(data.getValue0());
+ String suffix = SegmentDef.fixBehaviorSuffix(data.getValue0());
+ for (final String type : new String[] {"D", ""}) {
+ ResourceEntry entry = ResourceFactory.getResourceEntry(resref + type + suffix + ".BAM");
+ int cycle = data.getValue1().intValue() * seg;
+ ResourceEntry entryE = ResourceFactory.getResourceEntry(resref + type + suffix + suffixE + ".BAM");
+ int cycleE = cycle + eastOfs;
+
+ if (SpriteUtils.bamCyclesExist(entry, cycle, dirWest.length) &&
+ SpriteUtils.bamCyclesExist(entryE, cycleE, dirEast.length)) {
+ SeqDef tmp = SeqDef.createSequence(seq, dirWest, false, entry, cycle, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ tmp = SeqDef.createSequence(seq, dirEast, hasMirroredDirections(), entryE, cycleE, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+
+ if (retVal.isEmpty()) {
+ retVal = null;
+ }
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/MonsterDecoder.java b/src/org/infinity/resource/cre/decoder/MonsterDecoder.java
new file mode 100644
index 000000000..db90e6a78
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/MonsterDecoder.java
@@ -0,0 +1,336 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.itm.ItmResource;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type 7000 (monster) animations.
+ * Available ranges: (using notation slot/range where slot can be a formula with range definitions as [x,y])
+ * (0x7002 | ([0x00,0x1f] << 4))/0xd
+ * (0x7004 | ([0x20,0x2f] << 4))/0xb
+ * (0x7000 | ([0x30,0x3f] << 4))/0xf
+ * (0x7003 | ([0x40,0x4f] << 4))/0xc
+ * (0x7002 | ([0x50,0x5f] << 4))/0xd
+ * (0x7003 | ([0x70,0x7f] << 4))/0xc
+ * (0x7005 | ([0x90,0xaf] << 4))/0xa
+ * (0x7007 | ([0xb0,0xbf] << 4))/0x8
+ * (0x7002 | ([0xc0,0xcf] << 4))/0xd
+ * (0x7002 | ([0xe0,0xef] << 4))/0xd
+ * (0x7000 | ([0xf0,0xff] << 4))/0xf
+ */
+public class MonsterDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.MONSTER;
+
+ public static final DecoderAttribute KEY_CAN_LIE_DOWN = DecoderAttribute.with("can_lie_down", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_PATH_SMOOTH = DecoderAttribute.with("path_smooth", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_SPLIT_BAMS = DecoderAttribute.with("split_bams", DecoderAttribute.DataType.BOOLEAN);
+ public static final DecoderAttribute KEY_GLOW_LAYER = DecoderAttribute.with("glow_layer", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_PALETTE1 = DecoderAttribute.with("palette1", DecoderAttribute.DataType.STRING);
+ public static final DecoderAttribute KEY_PALETTE2 = DecoderAttribute.with("palette2", DecoderAttribute.DataType.STRING);
+
+ /** Assigns BAM suffix and cycle index to a specific animation sequence (unsplit version). */
+ private static final HashMap> suffixMapUnsplit = new HashMap>() {{
+ put(Sequence.WALK, Couple.with("G1", 0));
+ put(Sequence.STANCE, Couple.with("G1", 9));
+ put(Sequence.STAND, Couple.with("G1", 18));
+ put(Sequence.GET_HIT, Couple.with("G1", 27));
+ put(Sequence.DIE, Couple.with("G1", 36));
+ put(Sequence.TWITCH, Couple.with("G1", 45));
+ put(Sequence.SLEEP, Couple.with("G1", 54));
+ put(Sequence.GET_UP, Couple.with("G1", 63));
+ put(Sequence.ATTACK, Couple.with("G2", 0));
+ put(Sequence.ATTACK_2, Couple.with("G2", 9));
+ put(Sequence.ATTACK_3, Couple.with("G2", 18));
+ put(Sequence.ATTACK_4, Couple.with("G2", 27));
+ put(Sequence.ATTACK_5, Couple.with("G2", 36));
+ put(Sequence.SPELL, Couple.with("G2", 45));
+ put(Sequence.CAST, Couple.with("G2", 54));
+ }};
+
+ /** Assigns BAM suffix and cycle index to a specific animation sequence (split version). */
+ private static final HashMap> suffixMapSplit = new HashMap>() {{
+ put(Sequence.WALK, Couple.with("G11", 0));
+ put(Sequence.STANCE, Couple.with("G1", 9));
+ put(Sequence.STAND, Couple.with("G12", 18));
+ put(Sequence.GET_HIT, Couple.with("G13", 27));
+ put(Sequence.DIE, Couple.with("G14", 36));
+ put(Sequence.SLEEP, get(Sequence.DIE));
+ put(Sequence.GET_UP, Couple.with("!G14", 36));
+ put(Sequence.TWITCH, Couple.with("G15", 45));
+ put(Sequence.ATTACK, Couple.with("G2", 0));
+ put(Sequence.ATTACK_2, Couple.with("G21", 9));
+ put(Sequence.ATTACK_3, Couple.with("G22", 18));
+ put(Sequence.ATTACK_4, Couple.with("G23", 27));
+ put(Sequence.ATTACK_5, Couple.with("G24", 36));
+ put(Sequence.SPELL, Couple.with("G25", 45));
+ put(Sequence.CAST, Couple.with("G26", 54));
+ }};
+
+ /** Replacement sequences if original sequence definition is missing (unsplit version). */
+ private static final HashMap> replacementMapUnsplit = new HashMap>() {{
+ put(Sequence.DIE, suffixMapUnsplit.get(Sequence.SLEEP));
+ put(Sequence.SLEEP, suffixMapUnsplit.get(Sequence.DIE));
+ put(Sequence.GET_UP, Couple.with("!" + suffixMapUnsplit.get(Sequence.DIE).getValue0(), suffixMapUnsplit.get(Sequence.DIE).getValue1()));
+ }};
+
+ /** Replacement sequences if original sequence definition is missing (split version). */
+ private static final HashMap> replacementMapSplit = new HashMap>() {{
+ // not needed
+ }};
+
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int falseColor = SpriteTables.valueToInt(data, SpriteTables.COLUMN_CLOWN, 0);
+ int splitBams = SpriteTables.valueToInt(data, SpriteTables.COLUMN_SPLIT, 0);
+ int translucent = SpriteTables.valueToInt(data, SpriteTables.COLUMN_TRANSLUCENT, 0);
+ String palette1 = SpriteTables.valueToString(data, SpriteTables.COLUMN_PALETTE, "");
+ String palette2 = SpriteTables.valueToString(data, SpriteTables.COLUMN_PALETTE2, "");
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[monster]");
+ lines.add("false_color=" + falseColor);
+ lines.add("split_bams=" + splitBams);
+ lines.add("translucent=" + translucent);
+ lines.add("resref=" + resref);
+ if (!palette1.isEmpty()) {
+ lines.add("palette1=" + palette1);
+ }
+ if (!palette2.isEmpty()) {
+ lines.add("palette2=" + palette2);
+ }
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public MonsterDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public MonsterDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** Returns the correct sequence map for the current settings. */
+ private HashMap> getSuffixMap()
+ {
+ return isSplittedBams() ? suffixMapSplit : suffixMapUnsplit;
+ }
+
+ /** Returns whether the creature falls down when dead/unconscious. */
+ public boolean canLieDown() { return getAttribute(KEY_CAN_LIE_DOWN); }
+ protected void setCanLieDown(boolean b) { setAttribute(KEY_CAN_LIE_DOWN, b); }
+
+ /** ??? */
+ public boolean isSmoothPath() { return getAttribute(KEY_PATH_SMOOTH); }
+ protected void setSmoothPath(boolean b) { setAttribute(KEY_PATH_SMOOTH, b); }
+
+ /** Returns whether animations are spread over various subfiles. */
+ public boolean isSplittedBams() { return getAttribute(KEY_SPLIT_BAMS); }
+ protected void setSplittedBams(boolean b) { setAttribute(KEY_SPLIT_BAMS, b); }
+
+ /**
+ * Returns the solid background layer of blended/glowing creature animations.
+ * (Note: currently not supported by the engine.)
+ */
+ public String getGlowLayer() { return getAttribute(KEY_GLOW_LAYER); }
+ protected void setGlowLayer(String s) { setAttribute(KEY_GLOW_LAYER, s); }
+
+ /**
+ * Returns the first replacement palette (BMP) for the creature animation.
+ * Falls back to new palette from general attributes.
+ */
+ public String getPalette1()
+ {
+ String retVal = getAttribute(KEY_PALETTE1);
+ if (retVal.isEmpty()) {
+ retVal = getNewPalette();
+ }
+ return retVal;
+ }
+
+ protected void setPalette1(String s) { setAttribute(KEY_PALETTE1, s); }
+
+ /**
+ * Returns the second replacement palette (BMP) for the creature animation.
+ * Falls back to palette1 or new palette from general attributes.
+ */
+ public String getPalette2()
+ {
+ String retVal = getAttribute(KEY_PALETTE2);
+ if (retVal.isEmpty()) {
+ retVal = getPalette1();
+ }
+ return retVal;
+ }
+
+ protected void setPalette2(String s) { setAttribute(KEY_PALETTE2, s); }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ // collecting suffixes
+ String resref = getAnimationResref();
+ HashSet files = new HashSet<>();
+ for (final HashMap.Entry> entry : getSuffixMap().entrySet()) {
+ String suffix = SegmentDef.fixBehaviorSuffix(entry.getValue().getValue0());
+ files.add(resref + suffix + ".BAM");
+ }
+
+ // generating file list
+ ArrayList retVal = new ArrayList<>(Arrays.asList(files.toArray(new String[files.size()])));
+
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setCanLieDown(section.getAsInteger(KEY_CAN_LIE_DOWN.getName(), 0) != 0);
+ setFalseColor(section.getAsInteger(KEY_FALSE_COLOR.getName(), 0) != 0);
+ setDetectedByInfravision(section.getAsInteger(KEY_DETECTED_BY_INFRAVISION.getName(), 0) != 0);
+ setSmoothPath(section.getAsInteger(KEY_PATH_SMOOTH.getName(), 0) != 0);
+ setSplittedBams(section.getAsInteger(KEY_SPLIT_BAMS.getName(), 0) != 0);
+ setTranslucent(section.getAsInteger(KEY_TRANSLUCENT.getName(), 0) != 0);
+ String s = section.getAsString(KEY_GLOW_LAYER.getName(), "");
+ if (s.isEmpty()) {
+ // KEY_GLOW_LAYER maybe incorrectly assigned to "general" section
+ s = getGeneralIniSection(getAnimationInfo()).getAsString(KEY_GLOW_LAYER.getName(), "");
+ }
+ setGlowLayer(s);
+ setPalette1(section.getAsString(KEY_PALETTE1.getName(), ""));
+ setPalette2(section.getAsString(KEY_PALETTE2.getName(), ""));
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+
+ Couple data = getSuffixMap().get(seq);
+ if (data == null) {
+ return retVal;
+ }
+
+ ArrayList> creResList = new ArrayList<>();
+
+ // processing creature sprite
+ String resref = getAnimationResref();
+ String suffix = data.getValue0();
+ if (!SpriteUtils.bamCyclesExist(ResourceFactory.getResourceEntry(resref + SegmentDef.fixBehaviorSuffix(suffix) + ".BAM"),
+ data.getValue1().intValue(), 9)) {
+ data = (isSplittedBams() ? replacementMapSplit: replacementMapUnsplit).get(seq);
+ if (data == null) {
+ return retVal;
+ }
+ suffix = data.getValue0();
+ if (!ResourceFactory.resourceExists(resref + SegmentDef.fixBehaviorSuffix(suffix) + ".BAM")) {
+ return retVal;
+ }
+ }
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(suffix);
+ suffix = SegmentDef.fixBehaviorSuffix(suffix);
+ int ofs = data.getValue1().intValue();
+ creResList.add(Couple.with(resref + suffix + ".BAM", SegmentDef.SpriteType.AVATAR));
+
+ // processing weapon overlay
+ ItmResource itm = SpriteUtils.getEquippedWeapon(getCreResource());
+ if (itm != null) {
+ String weapon = SpriteUtils.getItemAppearance(itm).trim();
+ if (!weapon.isEmpty()) {
+ Couple wdata = suffixMapUnsplit.get(seq);
+ if (wdata != null) {
+ creResList.add(Couple.with(resref + wdata.getValue0() + weapon + ".BAM", SegmentDef.SpriteType.WEAPON));
+ }
+ }
+ }
+
+ retVal = new SeqDef(seq);
+ for (final Couple creInfo : creResList) {
+ ResourceEntry entry = ResourceFactory.getResourceEntry(creInfo.getValue0());
+ if (SpriteUtils.bamCyclesExist(entry, ofs, SeqDef.DIR_FULL_W.length)) {
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_FULL_W, false, entry, ofs, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ tmp = SeqDef.createSequence(seq, SeqDef.DIR_FULL_E, true, entry, ofs + 1, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ } else if (entry != null && SpriteUtils.getBamCycles(entry) == 1) {
+ // fallback solution: just use first bam cycle (required by a few animations)
+ SeqDef tmp = SeqDef.createSequence(seq, new Direction[] {Direction.S}, false, entry, 0, null, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+
+ if (retVal.isEmpty()) {
+ retVal = null;
+ }
+
+ return retVal;
+ }
+
+ @Override
+ protected int[] getNewPaletteData(ResourceEntry bamRes)
+ {
+ if (bamRes != null) {
+ String resref = bamRes.getResourceRef();
+ if (resref.length() >= 6) {
+ switch (resref.charAt(5)) {
+ case '1':
+ return SpriteUtils.loadReplacementPalette(getPalette1());
+ case '2':
+ return SpriteUtils.loadReplacementPalette(getPalette2());
+ }
+ }
+ }
+ return super.getNewPaletteData(bamRes);
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/MonsterIcewindDecoder.java b/src/org/infinity/resource/cre/decoder/MonsterIcewindDecoder.java
new file mode 100644
index 000000000..0d5ca619d
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/MonsterIcewindDecoder.java
@@ -0,0 +1,221 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.infinity.datatype.IsTextual;
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DecoderAttribute;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.itm.ItmResource;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+
+/**
+ * Creature animation decoder for processing type E000 (monster_icewind) animations.
+ * Available ranges: [e000,efff]
+ */
+public class MonsterIcewindDecoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.MONSTER_ICEWIND;
+
+ public static final DecoderAttribute KEY_WEAPON_LEFT_HAND = DecoderAttribute.with("weapon_left_hand", DecoderAttribute.DataType.BOOLEAN);
+
+ private static final HashMap seqMap = new HashMap() {{
+ put(Sequence.ATTACK, "A1");
+ put(Sequence.ATTACK_2, "A2");
+ put(Sequence.ATTACK_3, "A3");
+ put(Sequence.ATTACK_4, "A4");
+ put(Sequence.CAST, "CA");
+ put(Sequence.DIE, "DE");
+ put(Sequence.GET_HIT, "GH");
+ put(Sequence.GET_UP, "GU");
+ put(Sequence.STANCE, "SC");
+ put(Sequence.STAND, "SD");
+ put(Sequence.SLEEP, "SL");
+ put(Sequence.SPELL, "SP");
+ put(Sequence.TWITCH, "TW");
+ put(Sequence.WALK, "WK");
+ }};
+
+ private static final HashMap replacementMap = new HashMap() {{
+ put(Sequence.DIE, seqMap.get(Sequence.SLEEP));
+ put(Sequence.SLEEP, seqMap.get(Sequence.DIE));
+ put(Sequence.GET_UP, "!" + seqMap.get(Sequence.DIE));
+ }};
+
+ /**
+ * A helper method that parses the specified data array and generates a {@link IniMap} instance out of it.
+ * @param data a String array containing table values for a specific table entry.
+ * @return a {@code IniMap} instance with the value derived from the specified data array.
+ * Returns {@code null} if no data could be derived.
+ */
+ public static IniMap processTableData(String[] data)
+ {
+ IniMap retVal = null;
+ if (data == null || data.length < 16) {
+ return retVal;
+ }
+
+ String resref = SpriteTables.valueToString(data, SpriteTables.COLUMN_RESREF, "");
+ if (resref.isEmpty()) {
+ return retVal;
+ }
+ int translucent = SpriteTables.valueToInt(data, SpriteTables.COLUMN_TRANSLUCENT, 0);
+ int leftHanded = SpriteTables.valueToInt(data, SpriteTables.COLUMN_WEAPON, 0);
+
+ List lines = processTableDataGeneral(data, ANIMATION_TYPE);
+ lines.add("[monster_icewind]");
+ lines.add("weapon_left_hand=" + leftHanded);
+ lines.add("translucent=" + translucent);
+ lines.add("resref=" + resref);
+
+ retVal = IniMap.from(lines);
+
+ return retVal;
+ }
+
+ public MonsterIcewindDecoder(int animationId, IniMap ini) throws Exception
+ {
+ super(ANIMATION_TYPE, animationId, ini);
+ }
+
+ public MonsterIcewindDecoder(CreResource cre) throws Exception
+ {
+ super(ANIMATION_TYPE, cre);
+ }
+
+ /** ??? */
+ public boolean isWeaponInLeftHand() { return getAttribute(KEY_WEAPON_LEFT_HAND); }
+ protected void setWeaponInLeftHand(boolean b) { setAttribute(KEY_WEAPON_LEFT_HAND, b); }
+
+ @Override
+ public List getAnimationFiles(boolean essential)
+ {
+ ArrayList retVal = new ArrayList<>();
+ String resref = getAnimationResref();
+
+ final String[] defOvls = essential ? new String[] { "" }
+ : new String[] { "", "A", "B", "C", "D", "F", "H", "M", "Q", "S", "W" };
+ final String[] defSeqs = essential ? new String[] { "DE", "GH", "SD", "WK" }
+ : new String[] { "A1", "A2", "A3", "A4", "CA", "DE", "GH", "GU", "SC", "SD", "SL", "SP", "TW", "WK" };
+ for (final String ovl : defOvls) {
+ for (final String seq : defSeqs) {
+ String bamFile = resref + ovl + seq + ".BAM";
+ if (ResourceFactory.resourceExists(bamFile)) {
+ retVal.add(bamFile);
+ }
+ bamFile = resref + ovl + seq + "E.BAM";
+ if (ResourceFactory.resourceExists(bamFile)) {
+ retVal.add(bamFile);
+ }
+ }
+ }
+// final String[] wovl;
+// final String[] seqs;
+// if (essential) {
+// wovl = new String[] { "" };
+// seqs = new String[] { "DE", "GH", "SD", "WK" };
+// } else {
+// wovl = new String[] { "", "A", "B", "C", "D", "F", "H", "M", "Q", "S", "W" };
+// seqs = new String[] { "A1", "A2", "A3", "A4", "CA", "DE", "GH", "GU", "SC", "SD", "SL", "SP", "TW", "WK" };
+// }
+// for (final String wpn : wovl) {
+// for (final String seq : seqs) {
+// String bamFile = resref + wpn + seq + ".BAM";
+// retVal.add(resref + wpn + seq + "E.BAM");
+// }
+// }
+
+ return retVal;
+ }
+
+ @Override
+ public boolean isSequenceAvailable(Sequence seq)
+ {
+ return (getSequenceDefinition(seq) != null);
+ }
+
+ @Override
+ protected void init() throws Exception
+ {
+ // setting properties
+ initDefaults(getAnimationInfo());
+ IniMapSection section = getSpecificIniSection();
+ setWeaponInLeftHand(section.getAsInteger(KEY_WEAPON_LEFT_HAND.getName(), 0) != 0);
+ setTranslucent(section.getAsInteger(KEY_TRANSLUCENT.getName(), 0) != 0);
+ setDetectedByInfravision(section.getAsInteger(KEY_DETECTED_BY_INFRAVISION.getName(), 0) != 0);
+ }
+
+ @Override
+ protected SeqDef getSequenceDefinition(Sequence seq)
+ {
+ SeqDef retVal = null;
+ if (!seqMap.containsKey(seq)) {
+ return retVal;
+ }
+
+ String resref = getAnimationResref();
+
+ // getting weapon code from CRE resource
+ String weapon = "";
+ ItmResource itm = SpriteUtils.getEquippedWeapon(getCreResource());
+ if (itm != null) {
+ weapon = ((IsTextual)itm.getAttribute(ItmResource.ITM_EQUIPPED_APPEARANCE)).getText();
+ if (!weapon.isEmpty()) {
+ weapon = weapon.substring(0, 1).trim();
+ }
+ weapon = weapon.trim();
+ }
+
+ // checking availability of sequence
+ String suffix = seqMap.get(seq);
+ if (!ResourceFactory.resourceExists(resref + SegmentDef.fixBehaviorSuffix(suffix) + ".BAM")) {
+ suffix = replacementMap.get(seq);
+ if (!ResourceFactory.resourceExists(resref + SegmentDef.fixBehaviorSuffix(suffix) + ".BAM")) {
+ return retVal;
+ }
+ }
+
+ SegmentDef.Behavior behavior = SegmentDef.getBehaviorOf(suffix);
+ suffix = SegmentDef.fixBehaviorSuffix(suffix);
+
+ retVal = new SeqDef(seq);
+ String[] ovls = weapon.isEmpty() ? new String[] {""} : new String[] {"", weapon};
+ for (final String ovl : ovls) {
+ SegmentDef.SpriteType spriteType = (!weapon.isEmpty() && ovl.equals(weapon)) ? SegmentDef.SpriteType.WEAPON : SegmentDef.SpriteType.AVATAR;
+ ResourceEntry entry = ResourceFactory.getResourceEntry(resref + ovl + suffix + ".BAM");
+ if (entry != null) {
+ ResourceEntry entryE = ResourceFactory.getResourceEntry(resref + ovl + suffix + "E.BAM");
+ if (SpriteUtils.bamCyclesExist(entry, 0, SeqDef.DIR_REDUCED_W.length)) {
+ SeqDef tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_W, false, entry, 0, spriteType, behavior);
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ if (SpriteUtils.bamCyclesExist(entryE, 0, SeqDef.DIR_REDUCED_E.length)) {
+ tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_E, false, entryE, SeqDef.DIR_REDUCED_W.length, spriteType, behavior);
+ } else {
+ // fallback: mirror eastern directions
+ tmp = SeqDef.createSequence(seq, SeqDef.DIR_REDUCED_E, true, entry, 1, spriteType, behavior);
+ }
+ retVal.addDirections(tmp.getDirections().toArray(new DirDef[tmp.getDirections().size()]));
+ }
+ }
+ }
+
+ if (retVal.isEmpty()) {
+ retVal = null;
+ }
+
+ return retVal;
+ }
+}
diff --git a/src/org/infinity/resource/cre/decoder/MonsterLarge16Decoder.java b/src/org/infinity/resource/cre/decoder/MonsterLarge16Decoder.java
new file mode 100644
index 000000000..5019f61be
--- /dev/null
+++ b/src/org/infinity/resource/cre/decoder/MonsterLarge16Decoder.java
@@ -0,0 +1,144 @@
+// Near Infinity - An Infinity Engine Browser and Editor
+// Copyright (C) 2001 - 2021 Jon Olav Hauglid
+// See LICENSE.txt for license information
+
+package org.infinity.resource.cre.decoder;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.infinity.resource.ResourceFactory;
+import org.infinity.resource.cre.CreResource;
+import org.infinity.resource.cre.decoder.internal.DirDef;
+import org.infinity.resource.cre.decoder.internal.SegmentDef;
+import org.infinity.resource.cre.decoder.internal.SeqDef;
+import org.infinity.resource.cre.decoder.tables.SpriteTables;
+import org.infinity.resource.key.ResourceEntry;
+import org.infinity.util.IniMap;
+import org.infinity.util.IniMapSection;
+import org.infinity.util.tuples.Couple;
+
+/**
+ * Creature animation decoder for processing type A000 (monster_large16) animations.
+ * Available ranges: [a000,afff]
+ */
+public class MonsterLarge16Decoder extends SpriteDecoder
+{
+ /** The animation type associated with this class definition. */
+ public static final AnimationType ANIMATION_TYPE = AnimationType.MONSTER_LARGE_16;
+
+ private static final HashMap