diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml index 1fead9138..f1fda9b3a 100644 --- a/.github/workflows/ant.yml +++ b/.github/workflows/ant.yml @@ -23,6 +23,7 @@ jobs: sed -i 's/debug="false"/debug="true"/' build.xml ant -noinput -buildfile build.xml - name: Upload artifact + if: ${{ github.actor != 'NearInfinityBrowser' }} uses: pyTooling/Actions/releaser@r0 with: tag: nightly diff --git a/src/org/infinity/NearInfinity.java b/src/org/infinity/NearInfinity.java index dbea922e0..716c9958e 100644 --- a/src/org/infinity/NearInfinity.java +++ b/src/org/infinity/NearInfinity.java @@ -75,6 +75,7 @@ import javax.swing.plaf.FontUIResource; import org.infinity.datatype.ProRef; +import org.infinity.datatype.ResourceRef; import org.infinity.datatype.Song2daBitmap; import org.infinity.datatype.SpellProtType; import org.infinity.datatype.Summon2daBitmap; @@ -135,7 +136,7 @@ public final class NearInfinity extends JFrame implements ActionListener, ViewableContainer { // the current Near Infinity version - private static final String VERSION = "v2.4-20230714"; + private static final String VERSION = "v2.4-20230729"; // the minimum supported Java version private static final int JAVA_VERSION_MIN = 8; @@ -1503,9 +1504,8 @@ private void cacheResourceIcons(boolean threaded) { sizeList.add(IconCache.getDefaultListIconSize()); } if (!sizeList.isEmpty()) { - final String[] types = { "ITM", "SPL" }; int[] sizes = sizeList.stream().mapToInt(Integer::intValue).toArray(); - for (final String type : types) { + for (final String type : ResourceRef.getIconExtensions()) { final List resources = ResourceFactory.getResources(type); if (resources != null) { for (final ResourceEntry e : resources) { diff --git a/src/org/infinity/check/AbstractChecker.java b/src/org/infinity/check/AbstractChecker.java index c7ce88194..b21f07582 100644 --- a/src/org/infinity/check/AbstractChecker.java +++ b/src/org/infinity/check/AbstractChecker.java @@ -44,13 +44,20 @@ public abstract class AbstractChecker extends AbstractSearcher implements Action private final String key; /** Resources, selected for check. */ - protected List files; + private List files; - public AbstractChecker(String title, String key, String[] filetypes) { + public AbstractChecker(String title, String[] filetypes) { super(CHECK_MULTI_TYPE_FORMAT, NearInfinity.getInstance()); settingsWindow = new ChildFrame(title, true); settingsWindow.setIconImage(Icons.ICON_REFRESH_16.getIcon().getImage()); - this.key = key; + + // generating unique key for this checker type + String className = getClass().getSimpleName(); + if (className.isEmpty()) { + className = getClass().getName(); + } + this.key = className; + selector = new FileTypeSelector("Select files to check:", key, filetypes, null); final JPanel bpanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); @@ -85,6 +92,15 @@ protected boolean runCheck(List entries) { return runSearch("Checking", entries); } + /** + * Returns a list of resources for checking. + * + * @return List of resources of the selected types. + */ + protected List getFiles() { + return files; + } + @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bStart) { diff --git a/src/org/infinity/check/EffectValidationChecker.java b/src/org/infinity/check/EffectValidationChecker.java new file mode 100644 index 000000000..9cb0261ac --- /dev/null +++ b/src/org/infinity/check/EffectValidationChecker.java @@ -0,0 +1,79 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.check; + +import java.util.ArrayList; +import java.util.List; + +import org.infinity.NearInfinity; +import org.infinity.datatype.EffectBitmap; +import org.infinity.datatype.EffectType; +import org.infinity.datatype.IsNumeric; +import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; +import org.infinity.resource.Resource; +import org.infinity.resource.ResourceFactory; +import org.infinity.resource.StructEntry; +import org.infinity.resource.effects.BaseOpcode; +import org.infinity.resource.effects.DefaultOpcode; +import org.infinity.resource.key.ResourceEntry; +import org.infinity.search.ReferenceHitFrame; + +/** + * Checks for invalid effect opcodes. + */ +public class EffectValidationChecker extends AbstractChecker { + /** Window with check results. */ + private final ReferenceHitFrame hitFrame; + + public EffectValidationChecker() { + super("Effects Validation Checker", getSupportedResourceTypes()); + hitFrame = new ReferenceHitFrame("Invalid Effect Opcodes", NearInfinity.getInstance()); + } + + @Override + public void run() { + if (runCheck(getFiles())) { + hitFrame.close(); + } else { + hitFrame.setVisible(true); + } + } + + @Override + protected Runnable newWorker(ResourceEntry entry) { + return () -> { + final Resource resource = ResourceFactory.getResource(entry); + if (resource instanceof AbstractStruct) { + search(entry, (AbstractStruct) resource); + } + advanceProgress(); + }; + } + + private void search(ResourceEntry entry, AbstractStruct struct) { + for (final StructEntry field : struct.getFlatFields()) { + if (field instanceof EffectType || field instanceof EffectBitmap) { + int value = ((IsNumeric) field).getValue(); + final BaseOpcode opcode = BaseOpcode.getOpcode(value); + if (opcode instanceof DefaultOpcode) { + synchronized (hitFrame) { + hitFrame.addHit(entry, entry.getSearchString(), field); + } + } + } + } + } + + private static String[] getSupportedResourceTypes() { + final List retVal = new ArrayList<>(); + for (final String type : new String[] { "CRE", "EFF", "ITM", "SPL" }) { + if (Profile.isResourceTypeSupported(type)) { + retVal.add(type); + } + } + return retVal.toArray(new String[0]); + } +} diff --git a/src/org/infinity/check/EffectsIndexChecker.java b/src/org/infinity/check/EffectsIndexChecker.java index e7dfc793d..c8e5bcd94 100644 --- a/src/org/infinity/check/EffectsIndexChecker.java +++ b/src/org/infinity/check/EffectsIndexChecker.java @@ -20,7 +20,7 @@ public class EffectsIndexChecker extends AbstractChecker { private final ReferenceHitFrame hitFrame; public EffectsIndexChecker() { - super("Effects Index Checker", "EffectsIndexChecker", new String[] { "ITM", "SPL" }); + super("Effects Index Checker", new String[] { "ITM", "SPL" }); hitFrame = new ReferenceHitFrame("Mis-indexed Effects", NearInfinity.getInstance()); } @@ -28,7 +28,7 @@ public EffectsIndexChecker() { @Override public void run() { - if (runCheck(files)) { + if (runCheck(getFiles())) { hitFrame.close(); } else { hitFrame.setVisible(true); diff --git a/src/org/infinity/check/IDSRefChecker.java b/src/org/infinity/check/IDSRefChecker.java index 2759e8134..3e723b9c6 100644 --- a/src/org/infinity/check/IDSRefChecker.java +++ b/src/org/infinity/check/IDSRefChecker.java @@ -18,7 +18,7 @@ public final class IDSRefChecker extends AbstractChecker { private final ReferenceHitFrame hitFrame; public IDSRefChecker() { - super("IDSRef Checker", "IDSRefChecker", new String[] { "CRE", "EFF", "ITM", "PRO", "SPL" }); + super("IDSRef Checker", new String[] { "CRE", "EFF", "ITM", "PRO", "SPL" }); hitFrame = new ReferenceHitFrame("Unknown IDS references", NearInfinity.getInstance()); } @@ -26,7 +26,7 @@ public IDSRefChecker() { @Override public void run() { - if (runCheck(files)) { + if (runCheck(getFiles())) { hitFrame.close(); } else { hitFrame.setVisible(true); diff --git a/src/org/infinity/check/ResRefChecker.java b/src/org/infinity/check/ResRefChecker.java index 0c460ef3e..17baea633 100644 --- a/src/org/infinity/check/ResRefChecker.java +++ b/src/org/infinity/check/ResRefChecker.java @@ -28,7 +28,7 @@ public final class ResRefChecker extends AbstractChecker { private List extraValues; public ResRefChecker() { - super("ResRef Checker", "ResRefChecker", FILE_TYPES); + super("ResRef Checker", FILE_TYPES); hitFrame = new ReferenceHitFrame("Illegal ResourceRefs", NearInfinity.getInstance()); final ResourceEntry spawnRef = ResourceFactory.getResourceEntry("SPAWNGRP.2DA"); @@ -42,7 +42,7 @@ public ResRefChecker() { @Override public void run() { - if (runCheck(files)) { + if (runCheck(getFiles())) { hitFrame.close(); } else { hitFrame.setVisible(true); diff --git a/src/org/infinity/check/StringUseChecker.java b/src/org/infinity/check/StringUseChecker.java index 7f3bb22e3..93623347c 100644 --- a/src/org/infinity/check/StringUseChecker.java +++ b/src/org/infinity/check/StringUseChecker.java @@ -9,6 +9,8 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -57,9 +59,148 @@ import org.infinity.util.LuaParser; import org.infinity.util.Misc; import org.infinity.util.StringTable; +import org.infinity.util.Table2da; +import org.infinity.util.Table2daCache; public final class StringUseChecker extends AbstractSearcher implements Runnable, ListSelectionListener, SearchClient, ActionListener { + // List of 2DA table resrefs that should be ignored by the search + private static final HashSet BLACKLIST_2DA = new HashSet<>(Arrays.asList( + "ABCLASRQ", + "ABCLSMOD", + "ABDCDSRQ", + "ABDCSCRQ", + "ABRACEAD", + "ABRACERQ", + "ABSTART", + "ACHIEVEM", + "ALIGNMNT", + "AREALINK", + "AREALINS", + "BACKSTAB", + "BANTTIMG", + "BNTYCHNC", + "CACHVALD", + "CHRMODST", + "CLASCOLR", + "CLASISKL", + "CLASTHAC", + "CLASWEAP", + "CLEARAIR", + "CLEARTRP", + "CLEARWHR", + "CLSRCREQ", + "CLSSPLAB", + "CLSWPBON", + "CONCENTR", + "CONTAINR", + "CONTINGX", + "CRIPPSTR", + "CSOUND", + "DEATH", + "DEXMOD", + "DONARUMR", + "DUALCLAS", + "ENTRIES", + "EXTANIM", + "EXTSPEED", + "FALLEN", + "FAMILIAR", + "FATIGMOD", + "FOGAREA", + "FOGPT", + "HAPPY", + "HIDESPL", + "HPCLASS", + "HPCONBON", + "HPINIT", + "INTERACT", + "INTERDIA", + "INTMOD", + "INTOXCON", + "INTOXMOD", + "ITEMANIM", + "ITEMEXCL", + "ITEMSPEC", + "ITEMTYPE", + "ITMSLOTS", + "KITTABLE", + "LAYHANDS", + "LORE", + "LOREBON", + "LUABBR", + "LUNUMAB", + "LVLMODWM", + "MASTAREA", + "MGSRCREQ", + "MONKFIST", + "NPCLEVEL", + "NPCLVL25", + "NUMWSLOT", + "PARTYAI", + "PDIALOG", + "PORTRAIT", + "PPBEHAVE", + "PPLANE", + "PROFS", + "PROFSMAX", + "RACECOLR", + "RACEFEAT", + "RACEHATE", + "RACETHAC", + "RAISDEAD", + "RANDCOLR", + "REPMODST", + "REPSTART", + "REPUTATI", + "RMODCHR", + "RMODREP", + "RNDEQUIP", + "SKILLBRD", + "SKILLDEX", + "SKILLRAC", + "SKILLRNG", + "SKILLSHM", + "SLTSTEAL", + "SMTABLES", + "SNDCHANN", + "SNDENVRN", + "SNDRESRF", + "SNEAKATT", + "SONGLIST", + "SPAWNGRP", + "SPEECH", + "SPELLS", + "SPLAUTOP", + "SPLPROT", + "SPLSHMKN", + "SPLSRCKN", + "SPRKLCLR", + "STARTARE", + "STARTPOS", + "STATVAL", + "STRMOD", + "STRMODEX", + "STRTGOLD", + "STYLBONU", + "SUMMLIMT", + "T2DA0000", + "THAC0", + "THIEFSCL", + "THIEFSKL", + "TRAPLIMT", + "VARIMPRT", + "WISH", + "WMAPLAY", + "WSPATCK", + "WSPECIAL", + "XL3000", + "XPBONUS", + "XPCAP", + "XPLEVEL", + "XPLIST" + )); + private ChildFrame resultFrame; private JTextArea textArea; @@ -178,7 +319,14 @@ protected Runnable newWorker(ResourceEntry entry) { } else if (resource instanceof BcsResource) { checkScript((BcsResource) resource); } else if (resource instanceof PlainTextResource) { - checkTextfile((PlainTextResource) resource); + final PlainTextResource textResource = (PlainTextResource) resource; + if (entry.getExtension().equalsIgnoreCase("2DA")) { + check2da(Table2daCache.get(resource.getResourceEntry(), false)); + } else if (entry.getExtension().equalsIgnoreCase("MENU")) { + checkMenu(textResource.getText()); + } else { + checkTextfile(textResource); + } } else if (resource instanceof AbstractStruct) { checkStruct((AbstractStruct) resource); } @@ -261,6 +409,77 @@ private void checkTextfile(PlainTextResource text) { } } + private void check2da(Table2da table) { + if (table != null) { + final String resref = table.getResourceEntry().getResourceRef().toUpperCase(); + if (BLACKLIST_2DA.contains(resref)) { + return; + } + + // checking default value + final Number defValue = toNumber(table.getDefaultValue()); + if (defValue != null) { + updateStringUsed(defValue.longValue()); + } + + // checking table entries + for (int row = 0, numRows = table.getRowCount(); row < numRows; ++row) { + for (int col = 0, numCols = table.getColCount(row); col < numCols; ++col) { + final Number value = toNumber(table.getEntry(row, col).getValue()); + if (value != null) { + updateStringUsed(value.longValue()); + } + } + } + } + } + + private void checkMenu(String text) { + if (text != null) { + // Patterns to check... + // Note: Named-capturing group "number" must define the number pattern + final String[] patterns = { + // Match UI element property: text [strref] + "^[ \t]*text[ \t]*(?[0-9]+)", + // Match specific variable assignment: helpString = [strref] + "helpString[ \t]*=[ \t]*(?[0-9]+)", + // Match Infinity_FetchString([strref]) + "Infinity_FetchString\\([ \t]*(?[0-9]+)[ \t]*\\)", + // Match getTooltipWithHotkey(x,[strref]) + "getTooltipWithHotkey\\(.+,[ \t]*(?[0-9]+)[ \t]*\\)", + // Match return value of Lua functions + // Limiting to greater values to reduce number of false positives + "return[ \t]+(?[0-9]{2,})" + }; + + // Checking... + for (final String regex : patterns) { + final Pattern pattern = Pattern.compile(regex); + final Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + final Number nr = toNumber(matcher.group("number")); + if (nr != null) { + updateStringUsed(nr.longValue()); + } + } + } + + // Special check: strrefs in arrays + final Pattern pattern = Pattern.compile("\\{[^{}\\v]+\\}"); + final Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + final String subtext = matcher.group(); + final Matcher matcher2 = StringReferenceSearcher.NUMBER_PATTERN.matcher(subtext); + while (matcher2.find()) { + final Number nr = toNumber(matcher2.group()); + if (nr != null) { + updateStringUsed(nr.longValue()); + } + } + } + } + } + /** * Mark all strings from {@link StringTable string table} to which the script code refers, as used. *

@@ -335,6 +554,30 @@ private void checkBestiaryLua() { } } + /** + * Converts the specified string into a numeric value. + * + * @param s String containing a potential decimal or hexadecimal number. + * @return Number if conversion was successful, {@code null} otherwise. + */ + private Number toNumber(String s) { + Number retVal = null; + + if (s != null && !s.isEmpty()) { + try { + int radix = 10; + if (s.toLowerCase().startsWith("0x")) { + s = s.substring(2); + radix = 16; + } + retVal = Long.parseLong(s, radix); + } catch (Exception e) { + } + } + + return retVal; + } + // -------------------------- INNER CLASSES -------------------------- private static final class UnusedStringTableItem implements TableItem { diff --git a/src/org/infinity/check/StrrefIndexChecker.java b/src/org/infinity/check/StrrefIndexChecker.java index 7caa47015..18c9edc9c 100644 --- a/src/org/infinity/check/StrrefIndexChecker.java +++ b/src/org/infinity/check/StrrefIndexChecker.java @@ -63,7 +63,7 @@ public class StrrefIndexChecker extends AbstractChecker implements ListSelection private SortableTable table; public StrrefIndexChecker() { - super("Find illegal strrefs", "StrrefIndexChecker", StringReferenceSearcher.FILE_TYPES); + super("Find illegal strrefs", StringReferenceSearcher.FILE_TYPES); table = new SortableTable(new String[] { "File", "Offset / Line:Pos", "Strref" }, new Class[] { ResourceEntry.class, String.class, Integer.class }, new Integer[] { 200, 100, 100 }); @@ -108,7 +108,7 @@ public void valueChanged(ListSelectionEvent event) { @Override public void run() { - if (runCheck(files)) { + if (runCheck(getFiles())) { resultFrame.close(); return; } diff --git a/src/org/infinity/check/StructChecker.java b/src/org/infinity/check/StructChecker.java index f025d0644..f740bea03 100644 --- a/src/org/infinity/check/StructChecker.java +++ b/src/org/infinity/check/StructChecker.java @@ -85,7 +85,7 @@ public final class StructChecker extends AbstractChecker implements ListSelectio private final SortableTable table; public StructChecker() { - super("Find Corrupted Files", "StructChecker", FILETYPES); + super("Find Corrupted Files", FILETYPES); table = new SortableTable(new String[] { "File", "Offset", "Error message" }, new Class[] { ResourceEntry.class, String.class, String.class }, // TODO: replace "Offset" by Integer @@ -131,7 +131,7 @@ public void valueChanged(ListSelectionEvent event) { @Override public void run() { - if (runCheck(files)) { + if (runCheck(getFiles())) { resultFrame.close(); return; } diff --git a/src/org/infinity/datatype/EffectBitmap.java b/src/org/infinity/datatype/EffectBitmap.java new file mode 100644 index 000000000..bdf4a483c --- /dev/null +++ b/src/org/infinity/datatype/EffectBitmap.java @@ -0,0 +1,42 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.datatype; + +import java.nio.ByteBuffer; + +import org.infinity.resource.effects.BaseOpcode; + +/** + * Specialized {@link Bitmap} type that represents a list of available effect opcodes. + */ +public class EffectBitmap extends Bitmap { + public static final String EFFECT_FX = "Effect"; + + public EffectBitmap(ByteBuffer buffer, int offset, int length) { + this(buffer, offset, length, EFFECT_FX); + } + + public EffectBitmap(ByteBuffer buffer, int offset, int length, String name) { + super(buffer, offset, length, name, BaseOpcode.getEffectNames()); + } + + public EffectBitmap(ByteBuffer buffer, int offset, int length, boolean signed) { + this(buffer, offset, length, EFFECT_FX, signed); + } + + public EffectBitmap(ByteBuffer buffer, int offset, int length, String name, boolean signed) { + super(buffer, offset, length, name, BaseOpcode.getEffectNames(), signed); + } + + public EffectBitmap(ByteBuffer buffer, int offset, int length, boolean signed, + boolean showAsHex) { + this(buffer, offset, length, EFFECT_FX, signed, showAsHex); + } + + public EffectBitmap(ByteBuffer buffer, int offset, int length, String name, boolean signed, + boolean showAsHex) { + super(buffer, offset, length, name, BaseOpcode.getEffectNames(), signed, showAsHex); + } +} diff --git a/src/org/infinity/datatype/ResourceRef.java b/src/org/infinity/datatype/ResourceRef.java index c99d79969..99888d640 100644 --- a/src/org/infinity/datatype/ResourceRef.java +++ b/src/org/infinity/datatype/ResourceRef.java @@ -19,9 +19,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Set; import javax.swing.JButton; import javax.swing.JComponent; @@ -32,6 +34,7 @@ import org.infinity.gui.StructViewer; import org.infinity.gui.TextListPanel; import org.infinity.gui.ViewFrame; +import org.infinity.gui.ViewerUtil; import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; @@ -56,6 +59,10 @@ public class ResourceRef extends Datatype implements Editable, IsTextual, IsReference, ActionListener, ListSelectionListener { private static final Comparator IGNORE_CASE_EXT_COMPARATOR = new IgnoreCaseExtComparator(); + /** List of resource types that are can be used to display associated icons. */ + private static final HashSet ICON_EXTENSIONS = new HashSet<>( + Arrays.asList(new String[] { "BMP", "ITM", "SPL" })); + /** Special constant that represents absense of resource in the field. */ private static final ResourceRefEntry NONE = new ResourceRefEntry("None"); @@ -79,6 +86,14 @@ public class ResourceRef extends Datatype */ private TextListPanel list; + /** + * Returns a list of resource extensions that can be used to display associated icons. + * @return String set with file extensions (without leading dot). + */ + public static Set getIconExtensions() { + return Collections.unmodifiableSet(ICON_EXTENSIONS); + } + public ResourceRef(ByteBuffer buffer, int offset, String name, String... types) { this(buffer, offset, 8, name, types); } @@ -130,7 +145,7 @@ public JComponent edit(final ActionListener container) { addExtraEntries(values); Collections.sort(values, IGNORE_CASE_EXT_COMPARATOR); boolean showIcons = BrowserMenuBar.getInstance().getOptions().showResourceListIcons() && - Arrays.stream(types).anyMatch(s -> s.equalsIgnoreCase("ITM") || s.equalsIgnoreCase("SPL")); + Arrays.stream(types).anyMatch(s -> ICON_EXTENSIONS.contains(s.toUpperCase())); list = new TextListPanel<>(values, false, showIcons); list.addMouseListener(new MouseAdapter() { @Override @@ -170,33 +185,21 @@ public void mouseClicked(MouseEvent event) { bView.setEnabled(isEditable(list.getSelectedValue())); list.addListSelectionListener(this); - GridBagLayout gbl = new GridBagLayout(); - GridBagConstraints gbc = new GridBagConstraints(); - JPanel panel = new JPanel(gbl); - - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - gbc.gridheight = 2; - gbl.setConstraints(list, gbc); - panel.add(list); - - gbc.gridheight = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(3, 6, 3, 0); - gbc.anchor = GridBagConstraints.SOUTH; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbl.setConstraints(bUpdate, gbc); - panel.add(bUpdate); - - gbc.gridx = 1; - gbc.gridy = 1; - gbc.anchor = GridBagConstraints.NORTH; - gbl.setConstraints(bView, gbc); - panel.add(bView); + GridBagConstraints gbc = null; + JPanel panel = new JPanel(new GridBagLayout()); + + gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 2, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0); + panel.add(list, gbc); + gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 1.0, GridBagConstraints.SOUTH, GridBagConstraints.HORIZONTAL, + new Insets(3, 6, 3, 0), 0, 0); + panel.add(bUpdate, gbc); + gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 1.0, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, + new Insets(3, 6, 3, 0), 0, 0); + panel.add(bView, gbc); panel.setMinimumSize(Misc.getScaledDimension(DIM_MEDIUM)); + panel.setPreferredSize(panel.getMinimumSize()); return panel; } diff --git a/src/org/infinity/datatype/SpellProtType.java b/src/org/infinity/datatype/SpellProtType.java index a0e1bbe71..5668cd10d 100644 --- a/src/org/infinity/datatype/SpellProtType.java +++ b/src/org/infinity/datatype/SpellProtType.java @@ -13,6 +13,8 @@ import org.infinity.resource.Profile; import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructEntry; +import org.infinity.resource.are.AreResource; +import org.infinity.resource.cre.CreResource; import org.infinity.util.IdsMap; import org.infinity.util.IdsMapCache; import org.infinity.util.IdsMapEntry; @@ -136,8 +138,17 @@ public StructEntry createCreatureValueFromType(ByteBuffer buffer, int offset) { public StructEntry createCreatureValueFromType(ByteBuffer buffer, int offset, int size, String name) { if (useCustomValue()) { - String idsFile = getIdsFile(); - if (!idsFile.isEmpty()) { + final int stat = getSpellProtStat(); + final String idsFile = getIdsFile(); + if (stat == 0x106 && useCustomValue()) { + // AREATYPE.IDS as flags + return new Flag(buffer, offset, size, DEFAULT_NAME_VALUE, + IdsMapCache.getUpdatedIdsFlags(AreResource.FLAGS_ARRAY, idsFile, 2, false, false)); + } else if (stat == 0x111 && useCustomValue()) { + // STATE.IDS as flags + return CreResource.uniqueIdsFlag(new IdsFlag(buffer, offset, size, DEFAULT_NAME_VALUE, idsFile), + idsFile, '_'); + } else if (!idsFile.isEmpty()) { return new IdsBitmap(buffer, offset, size, createFieldName(name, index, DEFAULT_NAME_VALUE), idsFile); } else { return new DecNumber(buffer, offset, size, createFieldName(name, index, DEFAULT_NAME_VALUE)); @@ -199,6 +210,51 @@ public String getIdsFile() { return ""; } + /** + * Returns the STAT value associated with the current SPLPROT.2DA entry. + * + * @return STAT value of the current SPLPROT.2DA entry. Returns -1 for hardcoded entries. + */ + public int getSpellProtStat() { + if (isExternalized()) { + Table2da table = Table2daCache.get(TABLE_NAME); + if (table != null) { + return toNumber(table.get(getValue(), 1), -1); + } + } + return -1; + } + + /** + * Returns the VALUE value associated with the current SPLPROT.2DA entry. + * + * @return VALUE value of the current SPLPROT.2DA entry. Returns -1 for hardcoded entries. + */ + public int getSpellProtValue() { + if (isExternalized()) { + Table2da table = Table2daCache.get(TABLE_NAME); + if (table != null) { + return toNumber(table.get(getValue(), 2), -1); + } + } + return -1; + } + + /** + * Returns the RELATION value associated with the current SPLPROT.2DA entry. + * + * @return RELATION value of the current SPLPROT.2DA entry. Returns -1 for hardcoded entries. + */ + public int getSpellProtRelation() { + if (isExternalized()) { + Table2da table = Table2daCache.get(TABLE_NAME); + if (table != null) { + return toNumber(table.get(getValue(), 3), -1); + } + } + return -1; + } + /** Returns whether creature table has been externalized into a 2DA file. */ public static boolean isTableExternalized() { if (Profile.isEnhancedEdition()) { diff --git a/src/org/infinity/datatype/StringRef.java b/src/org/infinity/datatype/StringRef.java index aab3a6a65..66a25136b 100644 --- a/src/org/infinity/datatype/StringRef.java +++ b/src/org/infinity/datatype/StringRef.java @@ -17,7 +17,10 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import javax.swing.JButton; import javax.swing.JComponent; @@ -39,11 +42,18 @@ import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Profile; import org.infinity.resource.ResourceFactory; +import org.infinity.resource.key.BufferedResourceEntry; +import org.infinity.resource.key.FileResourceEntry; import org.infinity.resource.key.ResourceEntry; +import org.infinity.resource.sav.IOHandler; +import org.infinity.resource.sav.SavResourceEntry; +import org.infinity.resource.to.TohResource; import org.infinity.search.StringReferenceSearcher; import org.infinity.util.Misc; import org.infinity.util.StringTable; +import org.infinity.util.io.FileManager; /** * A struct field that represents reference to string in a talk table file (dialog.tlk or dialogF.tlk). @@ -58,6 +68,11 @@ */ public final class StringRef extends Datatype implements Editable, IsNumeric, IsTextual, ActionListener, ChangeListener { + /** + * If this value is defined then it overrides the default check and/or application of TLK syntax highlighting. + */ + private final InfinityTextArea.Language syntaxLanguageOverride; + /** * Button that opens dialog with sound associated with this reference if that sound exists. If no sound assotiated * with this string entry, button is disabled. @@ -94,8 +109,21 @@ public final class StringRef extends Datatype * @param value Index of the string in the talk table (TLK file) */ public StringRef(String name, int value) { + this(name, value, null); + } + + /** + * Constructs field description. + * + * @param name Name of field in parent struct that has {@code StringRef} type + * @param value Index of the string in the talk table (TLK file) + * @param languageOverride Specifies a syntax highlighting language that should be applied to the text view component. + * Overrides the default TLK highlighting if specified. + */ + public StringRef(String name, int value, InfinityTextArea.Language languageOverride) { super(0, 4, name); this.value = value; + this.syntaxLanguageOverride = languageOverride; } /** @@ -107,7 +135,22 @@ public StringRef(String name, int value) { * @param name Name of field */ public StringRef(ByteBuffer buffer, int offset, String name) { + this(buffer, offset, name, null); + } + + /** + * Constructs field description and reads its value from {@code buffer} starting with offset {@code offset}. Method + * reads 4 bytes from {@code buffer}. + * + * @param buffer Storage from which value of this field is readed + * @param offset Offset of this field in the {@code buffer} + * @param name Name of field + * @param languageOverride Specifies a syntax highlighting language that should be applied to the text view component. + * Overrides the default TLK highlighting if specified. + */ + public StringRef(ByteBuffer buffer, int offset, String name, InfinityTextArea.Language languageOverride) { super(offset, 4, name); + this.syntaxLanguageOverride = languageOverride; read(buffer, offset); } @@ -116,8 +159,8 @@ public StringRef(ByteBuffer buffer, int offset, String name) { @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bUpdate) { - taRefText.setText(StringTable.getStringRef(value)); - enablePlay(value); + taRefText.setText(getStringRef(value)); + updateButtonStates(value); } else if (event.getSource() == bEdit) { StringEditor.edit(value); } else if (event.getSource() == bPlay) { @@ -135,8 +178,8 @@ public void actionPerformed(ActionEvent event) { @Override public void stateChanged(ChangeEvent e) { value = getValueFromEditor(); - taRefText.setText(StringTable.getStringRef(value)); - enablePlay(value); + taRefText.setText(getStringRef(value)); + updateButtonStates(value); } // --------------------- End Interface ChangeListener --------------------- @@ -170,10 +213,12 @@ public void mouseClicked(MouseEvent e) { sRefNr.addChangeListener(this); taRefText = new InfinityTextArea(1, 200, true); - if (BrowserMenuBar.getInstance().getOptions().getTlkSyntaxHighlightingEnabled()) { + if (syntaxLanguageOverride != null) { + taRefText.applyExtendedSettings(syntaxLanguageOverride, null); + } else if (BrowserMenuBar.getInstance().getOptions().getTlkSyntaxHighlightingEnabled()) { taRefText.applyExtendedSettings(InfinityTextArea.Language.TLK, null); - taRefText.setFont(Misc.getScaledFont(taRefText.getFont())); } + taRefText.setFont(Misc.getScaledFont(taRefText.getFont())); taRefText.setHighlightCurrentLine(false); taRefText.setEditable(false); taRefText.setLineWrap(true); @@ -190,8 +235,8 @@ public void mouseClicked(MouseEvent e) { bSearch.addActionListener(this); bSearch.setMnemonic('f'); } - enablePlay(value); - taRefText.setText(StringTable.getStringRef(value)); + updateButtonStates(value); + taRefText.setText(getStringRef(value)); taRefText.setCaretPosition(0); InfinityScrollPane scroll = new InfinityScrollPane(taRefText, true); scroll.setLineNumbersEnabled(false); @@ -297,7 +342,7 @@ public String toString(StringTable.Format fmt) { if (fmt == null) { fmt = StringTable.Format.NONE; } - return StringTable.getStringRef(value, fmt); + return getStringRef(value, fmt); } @Override @@ -341,7 +386,7 @@ public int getValue() { @Override public String getText() { - return StringTable.getStringRef(value); + return getStringRef(value); } // --------------------- End Interface IsTextual --------------------- @@ -349,15 +394,26 @@ public String getText() { public void setValue(int newValue) { final int oldValue = value; value = newValue; - taRefText.setText(StringTable.getStringRef(newValue)); + taRefText.setText(getStringRef(newValue)); sRefNr.setValue(newValue); - enablePlay(newValue); + updateButtonStates(newValue); if (oldValue != newValue) { firePropertyChange(oldValue, newValue); } } + /** + * Enables or disables buttons in the string reference UI component depending on availability of the + * respective resource. + * + * @param value Value of string reference. + */ + private void updateButtonStates(int value) { + enablePlay(value); + enableEdit(value); + } + /** * Enables or disables button for view associated sound for specified StringRef value. * @@ -368,6 +424,15 @@ private void enablePlay(int value) { bPlay.setEnabled(!resname.isEmpty() && ResourceFactory.resourceExists(resname + ".WAV", true)); } + /** + * Enables or disables button for opening the string table editor with the specified StringRef value. + * + * @param value Value of string reference + */ + private void enableEdit(int value) { + bEdit.setEnabled(StringTable.isValidStringRef(value)); + } + /** * Extracts current value of string reference from editor. This value may not be saved yet in string field of * {@link #getParent() owner structure}, it is value of current string that editor is display. @@ -375,4 +440,66 @@ private void enablePlay(int value) { private int getValueFromEditor() { return ((Number) sRefNr.getValue()).intValue(); } + + private String getStringRef(int strref) { + return getStringRef(strref, StringTable.getDisplayFormat()); + } + + private String getStringRef(int strref, StringTable.Format fmt) { + String retVal = null; + + // overridden string? + if (strref >= 0) { + final ResourceEntry curEntry = ResourceFactory.getResourceEntry(this); + if (curEntry instanceof FileResourceEntry) { + final FileResourceEntry fileEntry = (FileResourceEntry) curEntry; + final Path basePath = fileEntry.getActualPath().getParent(); + ResourceEntry tohEntry = null; + ResourceEntry totEntry = null; + + final Path savFile = FileManager.resolveExisting(basePath.resolve(Profile.getProperty(Profile.Key.GET_SAV_NAME))); + if (savFile != null) { + // load TOH/TOT from SAV file + try { + final IOHandler handler = new IOHandler(new FileResourceEntry(savFile), false); + List overrideFiles = handler.getFileEntries().stream() + .filter(e -> e.getResourceName().equalsIgnoreCase("DEFAULT.TOH") || + e.getResourceName().equalsIgnoreCase("DEFAULT.TOT")) + .collect(Collectors.toList()); + for (final SavResourceEntry se : overrideFiles) { + if (se.getExtension().equalsIgnoreCase("TOH")) { + tohEntry = new BufferedResourceEntry(se.decompress(), se.getResourceName()); + } else if (se.getExtension().equalsIgnoreCase("TOT")) { + totEntry = new BufferedResourceEntry(se.decompress(), se.getResourceName()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } else { + // load TOH/TOT directly + final Path tohFile = FileManager.resolveExisting(basePath.resolve("DEFAULT.TOH")); + if (tohFile != null) { + tohEntry = new FileResourceEntry(tohFile); + } + final Path totFile = FileManager.resolveExisting(basePath.resolve("DEFAULT.TOT")); + if (totFile != null) { + totEntry = new FileResourceEntry(totFile); + } + } + + String text = TohResource.getOverrideString(tohEntry, totEntry, strref); + if (text != null) { + retVal = text; + } + } + } + + // regular string? + if (retVal == null) { + retVal = StringTable.getStringRef(strref, fmt); + } + + return retVal; + } } diff --git a/src/org/infinity/gui/ButtonPanel.java b/src/org/infinity/gui/ButtonPanel.java index 09216fab1..c3e44b94f 100644 --- a/src/org/infinity/gui/ButtonPanel.java +++ b/src/org/infinity/gui/ButtonPanel.java @@ -39,6 +39,8 @@ public enum Control { EXPORT_MENU, /** "Save" (JButton) */ SAVE, + /** "Save as..." (JButton) */ + SAVE_AS, /** "Add..." (ButtonPopupMenu) */ ADD, /** "Remove" (JButton) */ @@ -156,6 +158,12 @@ public static JComponent createControl(Control type) { retVal = b; break; } + case SAVE_AS: { + JButton b = new JButton("Save as...", Icons.ICON_SAVE_16.getIcon()); + b.setMnemonic('s'); + retVal = b; + break; + } case TRIM_SPACES: { retVal = new JButton("Trim spaces", Icons.ICON_REFRESH_16.getIcon()); break; diff --git a/src/org/infinity/gui/PreferencesDialog.java b/src/org/infinity/gui/PreferencesDialog.java index 585afb13b..547e5fc1c 100644 --- a/src/org/infinity/gui/PreferencesDialog.java +++ b/src/org/infinity/gui/PreferencesDialog.java @@ -174,14 +174,16 @@ public String toString() { + "in parentheses if available, such as creature, item or spell names.", AppOption.SHOW_TREE_SEARCH_NAMES), OptionCheckBox.create(AppOption.SHOW_RESOURCE_TREE_ICONS.getName(), AppOption.SHOW_RESOURCE_TREE_ICONS.getLabel(), - "With this option enabled Near Infinity shows icons alongside names in the resource tree for ITM and " - + "SPL resources." - + "

Caution: Enabling this option may result in noticeable lags on slower systems.

", + "With this option enabled Near Infinity shows icons alongside names in the resource tree for BMP, ITM " + + "and SPL resources." + + "

Caution: Enabling this option may result in increased memory usage and " + + "noticeable lags on slower systems.

", AppOption.SHOW_RESOURCE_TREE_ICONS), OptionCheckBox.create(AppOption.SHOW_RESOURCE_LIST_ICONS.getName(), AppOption.SHOW_RESOURCE_LIST_ICONS.getLabel(), "With this option enabled Near Infinity shows icons alongside names in resource selection lists and " - + "tables for ITM and SPL resources as well as portrait thumbnails for characters in GAM resources." - + "

Caution: Enabling this option may result in noticeable lags on slower systems.

", + + "tables for BMP, ITM and SPL resources as well as portrait thumbnails for characters in GAM resources." + + "

Caution: Enabling this option may result in increased memory usage and " + + "noticeable lags on slower systems.

", AppOption.SHOW_RESOURCE_LIST_ICONS), OptionCheckBox.create(AppOption.HIGHLIGHT_OVERRIDDEN.getName(), AppOption.HIGHLIGHT_OVERRIDDEN.getLabel(), "If checked, files that are listed in the chitin.key and are also available in the " diff --git a/src/org/infinity/gui/QuickSearch.java b/src/org/infinity/gui/QuickSearch.java index 0ec387cd3..98a83c87a 100644 --- a/src/org/infinity/gui/QuickSearch.java +++ b/src/org/infinity/gui/QuickSearch.java @@ -4,6 +4,7 @@ package org.infinity.gui; +import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -21,9 +22,10 @@ import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; -import javax.swing.JComboBox; import javax.swing.JLabel; +import javax.swing.JList; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.SwingConstants; @@ -54,6 +56,9 @@ private enum Result { CANCEL, OPEN, OPEN_NEW, } + // Max. number of visible rows in the popup menu list + private static final int MAX_ROW_COUNT = 12; + private final ButtonPopupWindow parent; private final ResourceTree tree; private final MapTree> resourceTree; @@ -61,7 +66,7 @@ private enum Result { private final JPanel mainPanel = new JPanel(new GridBagLayout()); private JLabel lSearch; - private JComboBox cbSearch; + private WideComboBox cbSearch; private JTextComponent tcEdit; private JButton bOk; private JButton bOkNew; @@ -164,7 +169,9 @@ public void popupWindowWillBecomeInvisible(PopupWindowEvent event) { lSearch = new JLabel("Search:", SwingConstants.LEFT); - cbSearch = new JComboBox<>(); + cbSearch = new WideComboBox<>(); + cbSearch.setRenderer(new QuickListCellRenderer()); + cbSearch.setFormatter(item -> QuickListCellRenderer.getFormattedValue(item)); cbSearch.setPreferredSize(Misc.getPrototypeSize(cbSearch, "WWWWWWWW.WWWW")); // space for at least 8.4 characters cbSearch.setEditable(true); tcEdit = (JTextComponent) cbSearch.getEditor().getEditorComponent(); @@ -395,7 +402,7 @@ public void run() { cbModel.addListDataListener(listener); } - cbSearch.setMaximumRowCount(Math.min(8, cbModel.getSize())); + cbSearch.setMaximumRowCount(Math.min(MAX_ROW_COUNT, cbModel.getSize())); if (cbModel.getSize() > 0 && !cbSearch.isPopupVisible()) { cbSearch.showPopup(); } else if (cbModel.getSize() == 0 && cbSearch.isPopupVisible()) { @@ -423,4 +430,40 @@ public void run() { } // --------------------- End Interface Runnable --------------------- + + // -------------------------- INNER CLASSES -------------------------- + + private static class QuickListCellRenderer extends DefaultListCellRenderer { + public QuickListCellRenderer() { + super(); + } + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + setText(getFormattedValue(value)); + return this; + } + + /** + * Returns the formatted string of {@code value} as it is provided by the renderer. + * + * @param value Value to be converted into a formatted string. + * @return Formatted string based on {@code value}. Returns empty string if {@code value} is {@code null}. + */ + public static String getFormattedValue(Object value) { + String retVal = ""; + if (value != null) { + retVal = value.toString(); + if (value instanceof ResourceEntry) { + final String name = ((ResourceEntry) value).getSearchString(); + if (name != null) { + retVal = retVal + " (" + name + ")"; + } + } + } + return retVal; + } + } } diff --git a/src/org/infinity/gui/ResourceTree.java b/src/org/infinity/gui/ResourceTree.java index 236eb6581..c1e61779d 100644 --- a/src/org/infinity/gui/ResourceTree.java +++ b/src/org/infinity/gui/ResourceTree.java @@ -6,6 +6,7 @@ import java.awt.BorderLayout; import java.awt.Component; +import java.awt.Cursor; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GridLayout; @@ -22,6 +23,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Stack; import javax.swing.Icon; @@ -35,18 +37,24 @@ import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingConstants; +import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; +import javax.swing.event.TreeWillExpandListener; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.infinity.NearInfinity; +import org.infinity.datatype.ResourceRef; import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.gui.menu.OverrideMode; import org.infinity.icon.Icons; @@ -68,6 +76,7 @@ public final class ResourceTree extends JPanel implements TreeSelectionListener, private final JButton bnext = new JButton("Forward", Icons.ICON_FORWARD_16.getIcon()); private final JButton bprev = new JButton("Back", Icons.ICON_BACK_16.getIcon()); private final JTree tree = new JTree(); + private final TreeExpandListener expandListener; private final Stack nextStack = new Stack<>(); private final Stack prevStack = new Stack<>(); @@ -76,6 +85,7 @@ public final class ResourceTree extends JPanel implements TreeSelectionListener, private boolean showResource = true; public ResourceTree(ResourceTreeModel treemodel) { + expandListener = new TreeExpandListener(tree); tree.setCellRenderer(new ResourceTreeRenderer()); tree.addKeyListener(new TreeKeyListener()); tree.addMouseListener(new TreeMouseListener()); @@ -182,13 +192,25 @@ public void select(ResourceEntry entry) { } public void select(ResourceEntry entry, boolean forced) { - if (entry == null) { - tree.clearSelection(); - } else if (forced || entry != shownResource) { - TreePath tp = ResourceFactory.getResourceTreeModel().getPathToNode(entry); - tree.scrollPathToVisible(tp); - tree.addSelectionPath(tp); - } + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + if (entry == null) { + tree.clearSelection(); + } else if (forced || entry != shownResource) { + TreePath tp = ResourceFactory.getResourceTreeModel().getPathToNode(entry); + try { + expandListener.treeWillExpand(new TreeExpansionEvent(tree, tp)); + tree.scrollPathToVisible(tp); + tree.addSelectionPath(tp); + tree.repaint(); + } finally { + expandListener.treeExpanded(new TreeExpansionEvent(tree, tp)); + } + } + return null; + } + }.execute(); } public ResourceTreeModel getModel() { @@ -493,6 +515,54 @@ private static Path getTempFile(Path file) { // -------------------------- INNER CLASSES -------------------------- + private final class TreeExpandListener implements TreeExpansionListener, TreeWillExpandListener { + private final JTree tree; + + private Cursor defaultCursor; + private boolean expanding; + + public TreeExpandListener(JTree tree) { + this.tree = Objects.requireNonNull(tree); + this.expanding = false; + if (this.tree != null) { + this.tree.addTreeWillExpandListener(this); + this.tree.addTreeExpansionListener(this); + } + } + + @Override + public void treeExpanded(TreeExpansionEvent event) { + synchronized (this) { + if (expanding) { + NearInfinity.getInstance().setCursor(defaultCursor); + defaultCursor = null; + expanding = false; + } + } + } + + @Override + public void treeCollapsed(TreeExpansionEvent event) { + // nothing to do + } + + @Override + public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { + synchronized (this) { + if (!expanding) { + defaultCursor = tree.getCursor(); + NearInfinity.getInstance().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + expanding = true; + } + } + } + + @Override + public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { + // nothing to do + } + } + private final class TreeKeyListener extends KeyAdapter implements ActionListener { private static final int TIMER_DELAY = 1000; private String currentkey = ""; @@ -729,8 +799,9 @@ public Component getTreeCellRendererComponent(JTree tree, Object o, boolean sel, Font font = tree.getFont(); if (leaf && o instanceof ResourceEntry) { final ResourceEntry e = (ResourceEntry) o; - boolean showIcon = BrowserMenuBar.getInstance().getOptions().showResourceTreeIcons() && - (e.getExtension().equalsIgnoreCase("ITM") || e.getExtension().equalsIgnoreCase("SPL")); + + final boolean showIcon = BrowserMenuBar.getInstance().getOptions().showResourceTreeIcons() && + ResourceRef.getIconExtensions().contains(e.getExtension()); final Icon icon = showIcon ? IconCache.get(e, iconSize) : e.getIcon(); final BrowserMenuBar options = BrowserMenuBar.getInstance(); diff --git a/src/org/infinity/gui/StructViewer.java b/src/org/infinity/gui/StructViewer.java index 904e9adc1..32ffb3406 100644 --- a/src/org/infinity/gui/StructViewer.java +++ b/src/org/infinity/gui/StructViewer.java @@ -403,6 +403,7 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole if (struct instanceof Resource && !struct.getFields().isEmpty() && struct.getParent() == null) { ((JButton) buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this); ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); + ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS)).addActionListener(this); } JScrollPane scrollTable = new JScrollPane(table); @@ -528,6 +529,10 @@ public void actionPerformed(ActionEvent event) { if (ResourceFactory.saveResource((Resource) struct, getTopLevelAncestor())) { struct.setStructChanged(false); } + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == event.getSource()) { + if (ResourceFactory.saveResourceAs((Resource) struct, getTopLevelAncestor())) { + struct.setStructChanged(false); + } } else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) { ResourceFactory.exportResource(struct.getResourceEntry(), getTopLevelAncestor()); } else if (buttonPanel.getControlByType(ButtonPanel.Control.PRINT) == event.getSource()) { diff --git a/src/org/infinity/gui/TextListPanel.java b/src/org/infinity/gui/TextListPanel.java index 192a668a7..77b29feda 100644 --- a/src/org/infinity/gui/TextListPanel.java +++ b/src/org/infinity/gui/TextListPanel.java @@ -14,12 +14,14 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseListener; +import java.io.FileNotFoundException; import java.util.Collections; import java.util.List; import java.util.Locale; import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; +import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JPanel; @@ -348,10 +350,14 @@ public Component getListCellRendererComponent(JList list, Object value, int i Object o = bmp.getDataOf(fmt.getValue()); if (o instanceof ResourceBitmap.RefEntry) { final ResourceBitmap.RefEntry entry = (ResourceBitmap.RefEntry) o; - final ResourceEntry iconEntry = ResourceFactory.getResourceIcon(entry.getResourceEntry()); - if (iconEntry != null) { - setIcon(IconCache.getIcon(iconEntry, IconCache.getDefaultListIconSize())); + ResourceEntry iconEntry = null; + Icon defIcon = null; + try { + iconEntry = ResourceFactory.getResourceIcon(entry.getResourceEntry()); + } catch (FileNotFoundException e) { + defIcon = ResourceFactory.getKeyfile().getIcon("BMP"); } + setIcon(IconCache.getIcon(iconEntry, IconCache.getDefaultListIconSize(), defIcon)); } } } diff --git a/src/org/infinity/gui/WideComboBox.java b/src/org/infinity/gui/WideComboBox.java new file mode 100644 index 000000000..375d40e4d --- /dev/null +++ b/src/org/infinity/gui/WideComboBox.java @@ -0,0 +1,167 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.gui; + +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Insets; +import java.util.Arrays; +import java.util.function.Function; + +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.UIManager; +import javax.swing.border.Border; + +/** + * A customized version of {@link JComboBox} that dynamically adapts the width of the popup menu to the + * width of available list items. + * + * @param the type of the elements of this combo box. + */ +class WideComboBox extends JComboBox { + /** Default string formatter for combo box elements. */ + private final Function defaultFormatter = item -> String.format("%s", item); + + private final int maxWidth; + private final int extraWidth; + + private Function formatter; + private boolean layingOut; + private boolean wide; + private int widestLength; + + /** + * Creates a new {@code WideComboBox} with a default data model. Adaptable width is enabled and + * maximum width of the popup menu is set to 80 instances of letter 'X'. + */ + public WideComboBox() { + this(true); + } + + /** + * Creates a new {@code WideComboBox} with a default data model. Maximum width of the popup menu + * is set to 80 instances of letter 'X'. + * + * @param wide whether adaptable width of the popup menu is enabled. + */ + public WideComboBox(boolean wide) { + this(wide, createString('X', 80)); + } + + /** + * Creates a new {@code WideComboBox} with a default data model. + * + * @param wide whether adaptable width of the popup menu is enabled. + * @param prototype String used to calculate the maximum width of the popup menu. + */ + public WideComboBox(boolean wide, String prototype) { + super(); + + maxWidth = getStringWidth(getFontMetrics(getFont()), getValue(prototype, "XXXXXXXX.XXXX")); + + // extraWidth is added to the width calculation of list items which includes scrollbars, borders, etc. + int borderWidth = 2; + final Border b = UIManager.getDefaults().getBorder("PopupMenu.border"); + if (b != null) { + final Insets insets = b.getBorderInsets(this); + if (insets != null) { + borderWidth = insets.left + insets.right; + } + } + extraWidth = UIManager.getDefaults().getInt("ScrollBar.width") + borderWidth + 2; // add some breathing space + + setFormatter(null); + setWide(wide); + } + + public boolean isWide() { + return wide; + } + + public void setWide(boolean wide) { + if (wide != this.wide) { + this.wide = wide; + this.widestLength = getWidestItemWidth(); + } + } + + /** Returns the string formatter for combo box elements. */ + public Function getFormatter() { + return formatter != null ? formatter : defaultFormatter; + } + + /** + * Sets a custom string formatter for combo box elements. + * + * @param formatter Function for converting combo box elements into strings. Specify {@code null} to use the + * default formatter. + */ + public void setFormatter(Function formatter) { + this.formatter = formatter; + } + + @Override + public void setPopupVisible(boolean v) { + if (wide && v && !isPopupVisible()) { + widestLength = getWidestItemWidth(); + } + super.setPopupVisible(v); + } + + @Override + public Dimension getSize() { + final Dimension dim = super.getSize(); + if (!layingOut && isWide()) { + dim.width = Math.max(widestLength, dim.width); + } + return dim; + } + + @Override + public void doLayout() { + try { + layingOut = true; + super.doLayout(); + } finally { + layingOut = false; + } + } + + private int getWidestItemWidth() { + final FontMetrics fm = getFontMetrics(getFont()); + final ComboBoxModel model = getModel(); + int widest = 0; + for (int i = 0, numItems = model.getSize(); i < numItems; i++) { + final E item = model.getElementAt(i); + final String text = getFormatter().apply(item); + final int lineWidth = fm.stringWidth(text); + widest = Math.min(Math.max(widest, lineWidth), maxWidth); + } + + return widest + extraWidth; + } + + /** Returns a {@code String} containing {@code count} instances of the character {@code letter}. */ + private static String createString(char letter, int count) { + final char[] buf = new char[Math.max(0, count)]; + Arrays.fill(buf, letter); + return new String(buf); + } + + /** Calculates the width of {@code text}, in pixels, based on the font described by {@code fm}. */ + private static int getStringWidth(FontMetrics fm, String text) { + int retVal = 0; + if (fm != null && text != null) { + retVal = fm.stringWidth(text); + } + return retVal; + } + + /** Returns {@code value} if non-{@code null}. Returns {@code defValue} otherwise. */ + private static T getValue(T value, T defValue) { + return (value != null) ? value : defValue; + } +} diff --git a/src/org/infinity/gui/hexview/StructHexViewer.java b/src/org/infinity/gui/hexview/StructHexViewer.java index 33ecfb8a5..30b35911b 100644 --- a/src/org/infinity/gui/hexview/StructHexViewer.java +++ b/src/org/infinity/gui/hexview/StructHexViewer.java @@ -98,6 +98,7 @@ public class StructHexViewer extends JPanel private static final ButtonPanel.Control BUTTON_FINDNEXT = ButtonPanel.Control.CUSTOM_1; private static final ButtonPanel.Control BUTTON_EXPORT = ButtonPanel.Control.EXPORT_BUTTON; private static final ButtonPanel.Control BUTTON_SAVE = ButtonPanel.Control.SAVE; + private static final ButtonPanel.Control BUTTON_SAVE_AS = ButtonPanel.Control.SAVE_AS; private static final ButtonPanel.Control BUTTON_REFRESH = ButtonPanel.Control.CUSTOM_2; private static final String FMT_OFFSET = "%1$Xh (%1$d)"; @@ -239,69 +240,13 @@ public void actionPerformed(ActionEvent event) { ResourceFactory.exportResource(getStruct().getResourceEntry(), getTopLevelAncestor()); getHexView().requestFocusInWindow(); } else if (event.getSource() == buttonPanel.getControlByType(BUTTON_SAVE)) { - // XXX: Ugly hack: mimicking ResourceFactory.saveResource() - IDataProvider dataProvider = getHexView().getData(); - ResourceEntry entry = getStruct().getResourceEntry(); - Path outPath; - if (entry instanceof BIFFResourceEntry) { - Path overridePath = FileManager.query(Profile.getGameRoot(), Profile.getOverrideFolderName()); - if (!FileEx.create(overridePath).isDirectory()) { - try { - Files.createDirectory(overridePath); - } catch (IOException e) { - JOptionPane.showMessageDialog(this, "Unable to create override folder.", "Error", - JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); - return; - } - } - outPath = FileManager.query(overridePath, entry.getResourceName()); - ((BIFFResourceEntry) entry).setOverride(true); - } else { - outPath = entry.getActualPath(); - } - if (FileEx.create(outPath).exists()) { - outPath = outPath.toAbsolutePath(); - String options[] = { "Overwrite", "Cancel" }; - if (JOptionPane.showOptionDialog(this, outPath + " exists. Overwrite?", "Save resource", - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) == 0) { - if (BrowserMenuBar.getInstance().getOptions().backupOnSave()) { - try { - Path bakPath = outPath.getParent().resolve(outPath.getFileName() + ".bak"); - if (FileEx.create(bakPath).isFile()) { - Files.delete(bakPath); - } - if (!FileEx.create(bakPath).exists()) { - Files.move(outPath, bakPath); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } else { - return; - } - } - - try { - byte[] buffer = dataProvider.getData(0, dataProvider.getDataLength()); - try (OutputStream os = StreamUtils.getOutputStream(outPath, true)) { - // make sure that nothing interferes with the writing process - getHexView().setEnabled(false); - StreamUtils.writeBytes(os, buffer); - } finally { - getHexView().setEnabled(true); - } - buffer = null; - getStruct().setStructChanged(false); - getHexView().clearModified(); - JOptionPane.showMessageDialog(this, "File saved to \"" + outPath.toAbsolutePath() + '\"', "Save complete", - JOptionPane.INFORMATION_MESSAGE); - } catch (IOException e) { - JOptionPane.showMessageDialog(this, "Error while saving " + getStruct().getResourceEntry().toString(), "Error", - JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); - return; + saveResource(null); + getHexView().requestFocusInWindow(); + } else if (event.getSource() == buttonPanel.getControlByType(BUTTON_SAVE_AS)) { + final Path outFile = ResourceFactory.getExportFileDialog(getTopLevelAncestor(), + getStruct().getResourceEntry().getResourceName(), true); + if (outFile != null) { + saveResource(outFile); } getHexView().requestFocusInWindow(); } else if (event.getSource() == buttonPanel.getControlByType(BUTTON_REFRESH)) { @@ -435,6 +380,8 @@ private void initGui() { b.addActionListener(this); b = (JButton) buttonPanel.addControl(BUTTON_SAVE); b.addActionListener(this); + b = (JButton) buttonPanel.addControl(BUTTON_SAVE_AS); + b.addActionListener(this); b = (JButton) buttonPanel.addControl(new JButton("Refresh"), BUTTON_REFRESH); b.setIcon(Icons.ICON_REFRESH_16.getIcon()); b.setToolTipText("Force a refresh of the displayed data"); @@ -524,6 +471,79 @@ private IColormap getColorMap() { return colorMap; } + private boolean saveResource(Path outFile) { + // XXX: Ugly hack: mimicking ResourceFactory.saveResourceInternal() + IDataProvider dataProvider = getHexView().getData(); + ResourceEntry entry = getStruct().getResourceEntry(); + Path outPath; + if (outFile != null) { + outPath = outFile; + } else { + if (entry instanceof BIFFResourceEntry) { + Path overridePath = FileManager.query(Profile.getGameRoot(), Profile.getOverrideFolderName()); + if (!FileEx.create(overridePath).isDirectory()) { + try { + Files.createDirectory(overridePath); + } catch (IOException e) { + JOptionPane.showMessageDialog(this, "Unable to create override folder.", "Error", + JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + return false; + } + } + outPath = FileManager.query(overridePath, entry.getResourceName()); + ((BIFFResourceEntry) entry).setOverride(true); + } else { + outPath = entry.getActualPath(); + } + } + + if (FileEx.create(outPath).exists()) { + outPath = outPath.toAbsolutePath(); + String options[] = { "Overwrite", "Cancel" }; + if (JOptionPane.showOptionDialog(this, outPath + " exists. Overwrite?", "Save resource", + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) == 0) { + if (BrowserMenuBar.getInstance().getOptions().backupOnSave()) { + try { + Path bakPath = outPath.getParent().resolve(outPath.getFileName() + ".bak"); + if (FileEx.create(bakPath).isFile()) { + Files.delete(bakPath); + } + if (!FileEx.create(bakPath).exists()) { + Files.move(outPath, bakPath); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + return false; + } + } + + try { + byte[] buffer = dataProvider.getData(0, dataProvider.getDataLength()); + try (OutputStream os = StreamUtils.getOutputStream(outPath, true)) { + // make sure that nothing interferes with the writing process + getHexView().setEnabled(false); + StreamUtils.writeBytes(os, buffer); + } finally { + getHexView().setEnabled(true); + } + buffer = null; + getStruct().setStructChanged(false); + getHexView().clearModified(); + JOptionPane.showMessageDialog(this, "File saved to \"" + outPath.toAbsolutePath() + '\"', "Save complete", + JOptionPane.INFORMATION_MESSAGE); + } catch (IOException e) { + JOptionPane.showMessageDialog(this, "Error while saving " + getStruct().getResourceEntry().toString(), "Error", + JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + return false; + } + return true; + } + // -------------------------- INNER CLASSES -------------------------- /** Panel component showing information about the currently selected data. */ diff --git a/src/org/infinity/gui/layeritem/AbstractLayerItem.java b/src/org/infinity/gui/layeritem/AbstractLayerItem.java index 25c983a0b..24a08c1d4 100644 --- a/src/org/infinity/gui/layeritem/AbstractLayerItem.java +++ b/src/org/infinity/gui/layeritem/AbstractLayerItem.java @@ -33,20 +33,35 @@ public enum ItemState { private final Vector actionListener = new Vector<>(); private final Vector itemStateListener = new Vector<>(); private final Viewable viewable; + private final Point center; + private final int id; + private Object objData; private ItemState itemState; - private final Point center; /** * Initialize object with a associated viewable object and message for both info box and quick info. + * The item is automatically associated with the primary sublayer. * * @param viewable Associated Viewable object * @param tooltip A short text message shown as tooltip or menu item text */ public AbstractLayerItem(Viewable viewable, String tooltip) { + this(viewable, tooltip, 0); + } + + /** + * Initialize object with a associated viewable object and message for both info box and quick info. + * + * @param viewable Associated Viewable object + * @param tooltip A short text message shown as tooltip or menu item text + * @param id Identifier that associates this item with a specific sublayer. + */ + public AbstractLayerItem(Viewable viewable, String tooltip, int id) { this.viewable = viewable; this.itemState = ItemState.NORMAL; this.center = new Point(); + this.id = id; if (viewable instanceof StructEntry) { setToolTipText(((StructEntry) viewable).getName() + ": " + tooltip); } else { @@ -115,6 +130,15 @@ public ItemState getItemState() { return itemState; } + /** + * Returns the sublayer identifier associated with this layer item. + * + * @return Sublayer iIdentifier for this layer item. + */ + public int getId() { + return id; + } + /** * Attaches a custom data object to this layer item. * diff --git a/src/org/infinity/gui/layeritem/IconLayerItem.java b/src/org/infinity/gui/layeritem/IconLayerItem.java index 1c7af85c3..37362d5cf 100644 --- a/src/org/infinity/gui/layeritem/IconLayerItem.java +++ b/src/org/infinity/gui/layeritem/IconLayerItem.java @@ -45,6 +45,17 @@ public class IconLayerItem extends AbstractLayerItem implements LayerItemListene * @param message An arbitrary text message */ public IconLayerItem(Viewable viewable, String message) { + this(viewable, message, 0); + } + + /** + * Initialize object with an associated Viewable and an additional text message. + * + * @param viewable Associated Viewable object + * @param message An arbitrary text message + * @param id Identifier that associates this item with a specific sublayer. + */ + public IconLayerItem(Viewable viewable, String message, int id) { this(viewable, message, null, null); } @@ -58,7 +69,21 @@ public IconLayerItem(Viewable viewable, String message) { * @param center Logical center position within the icon */ public IconLayerItem(Viewable viewable, String tooltip, Image image, Point center) { - super(viewable, tooltip); + this(viewable, tooltip, 0, image, center); + } + + /** + * Initialize object with an associated Viewable, an additional text message, an image for the visual representation + * and a locical center position within the icon. + * + * @param viewable Associated Viewable object + * @param tooltip A short text message shown as tooltip or menu item text + * @param id Identifier that associates this item with a specific sublayer. + * @param image The image to display + * @param center Logical center position within the icon + */ + public IconLayerItem(Viewable viewable, String tooltip, int id, Image image, Point center) { + super(viewable, tooltip, id); setLayout(new BorderLayout()); // preparing icon rcCanvas = new FrameCanvas(this); diff --git a/src/org/infinity/gui/layeritem/ShapedLayerItem.java b/src/org/infinity/gui/layeritem/ShapedLayerItem.java index 98549082d..117f1b1df 100644 --- a/src/org/infinity/gui/layeritem/ShapedLayerItem.java +++ b/src/org/infinity/gui/layeritem/ShapedLayerItem.java @@ -44,6 +44,19 @@ public class ShapedLayerItem extends AbstractLayerItem implements LayerItemListe * @param shape The shape to display */ public ShapedLayerItem(Viewable viewable, String tooltip, Shape shape) { + this(viewable, tooltip, 0, shape); + } + + /** + * Initialize object with an associated Viewable, an additional text message and a shape for the visual + * representation. + * + * @param viewable Associated Viewable object + * @param tooltip A short text message shown as tooltip or menu item text + * @param id Identifier that associates this item with a specific sublayer. + * @param shape The shape to display + */ + public ShapedLayerItem(Viewable viewable, String tooltip, int id, Shape shape) { super(viewable, tooltip); setLayout(new BorderLayout()); label = new ShapeLabel(this); diff --git a/src/org/infinity/gui/menu/ToolsMenu.java b/src/org/infinity/gui/menu/ToolsMenu.java index 11b90c739..9765632c9 100644 --- a/src/org/infinity/gui/menu/ToolsMenu.java +++ b/src/org/infinity/gui/menu/ToolsMenu.java @@ -20,6 +20,7 @@ import org.infinity.check.BCSIDSChecker; import org.infinity.check.CreInvChecker; import org.infinity.check.DialogChecker; +import org.infinity.check.EffectValidationChecker; import org.infinity.check.EffectsIndexChecker; import org.infinity.check.IDSRefChecker; import org.infinity.check.ResRefChecker; @@ -79,6 +80,7 @@ public class ToolsMenu extends JMenu implements BrowserSubMenu, ActionListener { private final JMenuItem toolMassExport; private final JMenuItem toolCheckEffectsIndex; + private final JMenuItem toolCheckEffectIsValid; private final JMenuItem toolConvImageToBam; private final JMenuItem toolConvImageToBmp; @@ -190,6 +192,11 @@ public ToolsMenu(BrowserMenuBar parent) { toolCheckEffectsIndex = BrowserMenuBar.makeMenuItem("For Mis-indexed Effects...", -1, Icons.ICON_FIND_16.getIcon(), -1, this); checkMenu.add(toolCheckEffectsIndex); + + toolCheckEffectIsValid = BrowserMenuBar.makeMenuItem("For Invalid Effect Opcodes...", -1, + Icons.ICON_FIND_16.getIcon(), -1, this); + toolCheckEffectIsValid.setToolTipText("Reports opcodes that are unknown or not supported by this game"); + checkMenu.add(toolCheckEffectIsValid); // *** End Check submenu *** // *** Begin Convert submenu *** @@ -370,6 +377,8 @@ public void windowClosing(WindowEvent e) { new MassExporter(); } else if (event.getSource() == toolCheckEffectsIndex) { new EffectsIndexChecker(); + } else if (event.getSource() == toolCheckEffectIsValid) { + new EffectValidationChecker(); } else if (event.getSource() == toolConvImageToPvrz) { ChildFrame.show(ConvertToPvrz.class, ConvertToPvrz::new); } else if (event.getSource() == toolConvImageToTis) { diff --git a/src/org/infinity/resource/AbstractStruct.java b/src/org/infinity/resource/AbstractStruct.java index b45b6b77a..f025e3462 100644 --- a/src/org/infinity/resource/AbstractStruct.java +++ b/src/org/infinity/resource/AbstractStruct.java @@ -91,6 +91,11 @@ public abstract class AbstractStruct extends AbstractTableModel */ private String name; + /** + * Arbitrary data that is associated with this structure. + */ + private final Object extraData; + private StructViewer viewer; private boolean structChanged; @@ -153,7 +158,20 @@ private static void adjustSectionOffsets(AbstractStruct superStruct, AddRemovabl * @throws Exception If resource can not be readed */ protected AbstractStruct(ResourceEntry entry) throws Exception { + this(entry, null); + } + + /** + * Creates top-level struct, that represents specified resource. Reads specified resource and creates it structured + * representation. + * + * @param entry Pointer to resource for read + * @param extraData Arbitrary data for use in derived classes or by explicit queries. + * @throws Exception If resource can not be readed + */ + protected AbstractStruct(ResourceEntry entry, Object extraData) throws Exception { this.entry = entry; + this.extraData = extraData; fields = new ArrayList<>(); name = entry.getResourceName(); ByteBuffer bb = entry.getResourceBuffer(); @@ -167,6 +185,7 @@ protected AbstractStruct(ResourceEntry entry) throws Exception { protected AbstractStruct(AbstractStruct superStruct, String name, int startoffset, int listSize) { this.entry = null; + this.extraData = null; this.superStruct = superStruct; this.name = name; this.startoffset = startoffset; @@ -813,10 +832,24 @@ public List getFlatFields() { return flatList; } + /** + * Returns the {@link ResourceEntry} associated with this structure. May return {@code null} if the structure + * is not directly associated with a resource. + */ public ResourceEntry getResourceEntry() { return entry; } + /** + * Returns arbitrary data that can be assigned to this structure in the constructor. + * + * This is a workaround to associate data for methods that are called by the {@code AbstractStruct} constructor + * before the derived class can process the data further. + */ + public Object getExtraData() { + return extraData; + } + public AbstractStruct getSuperStruct(StructEntry structEntry) { for (final StructEntry e : fields) { if (e == structEntry) { diff --git a/src/org/infinity/resource/Profile.java b/src/org/infinity/resource/Profile.java index 1e365aef8..809596f41 100644 --- a/src/org/infinity/resource/Profile.java +++ b/src/org/infinity/resource/Profile.java @@ -469,6 +469,8 @@ public enum Key { GET_IDS_ALIGNMENT, /** Property: ({@code String}) The name of the .GAM file that is stored in saved games. */ GET_GAM_NAME, + /** Property: ({@code String}) The name of the .SAV file that is stored in saved games. */ + GET_SAV_NAME, /** Property: ({@code Boolean}) Indices whether overlays in tilesets are stenciled. */ IS_TILESET_STENCILED, } @@ -2375,15 +2377,19 @@ private void initFeatures() { switch (engine) { case IWD: addEntry(Key.GET_GAM_NAME, Type.STRING, "ICEWIND.GAM"); + addEntry(Key.GET_SAV_NAME, Type.STRING, "ICEWIND.SAV"); break; case IWD2: addEntry(Key.GET_GAM_NAME, Type.STRING, "ICEWIND2.GAM"); + addEntry(Key.GET_SAV_NAME, Type.STRING, "ICEWIND2.SAV"); break; case PST: addEntry(Key.GET_GAM_NAME, Type.STRING, "TORMENT.GAM"); + addEntry(Key.GET_SAV_NAME, Type.STRING, "TORMENT.SAV"); break; default: addEntry(Key.GET_GAM_NAME, Type.STRING, "BALDUR.GAM"); + addEntry(Key.GET_SAV_NAME, Type.STRING, "BALDUR.SAV"); } // display mode of overlays in tilesets diff --git a/src/org/infinity/resource/ResourceFactory.java b/src/org/infinity/resource/ResourceFactory.java index fe33a92cd..c6d793c3a 100644 --- a/src/org/infinity/resource/ResourceFactory.java +++ b/src/org/infinity/resource/ResourceFactory.java @@ -9,6 +9,7 @@ import java.awt.HeadlessException; import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -104,7 +105,7 @@ public final class ResourceFactory implements FileWatchListener { /** * Name of tree node that contains important game files that not stored in the BIF archives or override folders. */ - private static final String SPECIAL_CATEGORY = "Special"; + public static final String SPECIAL_CATEGORY = "Special"; private static ResourceFactory instance; @@ -276,9 +277,10 @@ public static Resource getResource(ResourceEntry entry, String forcedExtension) * * @param entry The {@code ResourceEntry} of the resource. * @return BAM {@link ResourceEntry} of the icon associated with the resource. Returns {@code null} if icon is not - * available. + * defined by the resource. + * @throws FileNotFoundException if an icon resource is defined but doesn't exist in the game. */ - public static ResourceEntry getResourceIcon(ResourceEntry entry) { + public static ResourceEntry getResourceIcon(ResourceEntry entry) throws FileNotFoundException { return getResourceIcon(entry, null); } @@ -291,9 +293,10 @@ public static ResourceEntry getResourceIcon(ResourceEntry entry) { * @param entry The {@code ResourceEntry} of the resource. * @param forcedExtension Optional file extension string that is used to override the original file type. * @return BAM {@link ResourceEntry} of the icon associated with the resource. Returns {@code null} if icon is not - * available. + * defined by the resource. + * @throws FileNotFoundException if an icon resource is defined but doesn't exist in the game. */ - public static ResourceEntry getResourceIcon(ResourceEntry entry, String forcedExtension) { + public static ResourceEntry getResourceIcon(ResourceEntry entry, String forcedExtension) throws FileNotFoundException { ResourceEntry retVal = null; Class clsResource = getResourceType(entry, forcedExtension); @@ -306,8 +309,14 @@ public static ResourceEntry getResourceIcon(ResourceEntry entry, String forcedEx final ResourceEntry iconEntry = ResourceFactory.getResourceEntry(iconResref + ".BAM"); if (iconEntry != null) { retVal = iconEntry; + } else { + // invalid resref + throw new FileNotFoundException("Resource does not exist: " + iconResref + ".BAM"); } } + } catch (FileNotFoundException e) { + // forward exception to caller + throw e; } catch (Exception e) { e.printStackTrace(); } @@ -545,6 +554,30 @@ public static ResourceEntry getResourceEntry(String resourceName, boolean search } } + /** + * Attempts to determine the parent {@link ResourceEntry} of the specified {@link StructEntry} instance. + * + * @param struct A {@link StructEntry} instance. + * @return {@code ResourceEntry} of the resource containing {@code struct}. Returns {@code null} if resource could not + * be determined. + */ + public static ResourceEntry getResourceEntry(StructEntry struct) { + ResourceEntry entry = null; + + while (struct != null) { + if (struct instanceof AbstractStruct) { + final AbstractStruct as = (AbstractStruct) struct; + if (as.getResourceEntry() != null) { + entry = as.getResourceEntry(); + break; + } + } + struct = struct.getParent(); + } + + return entry; + } + /** Returns the resource tree model of the current game. */ public static ResourceTreeModel getResourceTreeModel() { if (getInstance() != null) { @@ -665,6 +698,14 @@ public static boolean saveResource(Resource resource, Component parent) { } } + public static boolean saveResourceAs(Resource resource, Component parent) { + if (getInstance() != null) { + return getInstance().saveResourceAsInternal(resource, parent); + } else { + return false; + } + } + /** * If {@code output} is not {@code null}, shows confirmation dialog for saving resource. If user accepts saving then * resource will be saved if it implements {@link Writable} @@ -1540,7 +1581,20 @@ private void saveCopyOfResourceInternal(ResourceEntry entry) { } } + private boolean saveResourceAsInternal(Resource resource, Component parent) { + final Path outFile = getExportFileDialogInternal(parent, resource.getResourceEntry().getResourceName(), true); + if (outFile != null) { + return saveResourceInternal(resource, parent, outFile); + } else { + return false; + } + } + private boolean saveResourceInternal(Resource resource, Component parent) { + return saveResourceInternal(resource, parent, null); + } + + private boolean saveResourceInternal(Resource resource, Component parent, Path outFile) { if (!(resource instanceof Writeable)) { JOptionPane.showMessageDialog(parent, "Resource not savable", "Error", JOptionPane.ERROR_MESSAGE); return false; @@ -1549,38 +1603,44 @@ private boolean saveResourceInternal(Resource resource, Component parent) { if (entry == null) { return false; } + Path outPath; - if (entry instanceof BIFFResourceEntry) { - Path overridePath = FileManager.query(Profile.getGameRoot(), Profile.getOverrideFolderName()); - if (!FileEx.create(overridePath).isDirectory()) { - try { - Files.createDirectory(overridePath); - } catch (IOException e) { - JOptionPane.showMessageDialog(parent, "Unable to create override folder.", "Error", - JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); - return false; - } - } - outPath = FileManager.query(overridePath, entry.getResourceName()); - ((BIFFResourceEntry) entry).setOverride(true); + if (outFile != null) { + outPath = outFile; } else { - outPath = entry.getActualPath(); - // extra step for saving resources from a read-only medium (such as DLCs) - if (!FileManager.isDefaultFileSystem(outPath)) { - outPath = Profile.getGameRoot().resolve(outPath.subpath(0, outPath.getNameCount()).toString()); - if (outPath != null && !FileEx.create(outPath.getParent()).exists()) { + if (entry instanceof BIFFResourceEntry) { + Path overridePath = FileManager.query(Profile.getGameRoot(), Profile.getOverrideFolderName()); + if (!FileEx.create(overridePath).isDirectory()) { try { - Files.createDirectories(outPath.getParent()); + Files.createDirectory(overridePath); } catch (IOException e) { - JOptionPane.showMessageDialog(parent, "Unable to create folder: " + outPath.getParent(), "Error", + JOptionPane.showMessageDialog(parent, "Unable to create override folder.", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); return false; } } + outPath = FileManager.query(overridePath, entry.getResourceName()); + ((BIFFResourceEntry) entry).setOverride(true); + } else { + outPath = entry.getActualPath(); + // extra step for saving resources from a read-only medium (such as DLCs) + if (!FileManager.isDefaultFileSystem(outPath)) { + outPath = Profile.getGameRoot().resolve(outPath.subpath(0, outPath.getNameCount()).toString()); + if (outPath != null && !FileEx.create(outPath.getParent()).exists()) { + try { + Files.createDirectories(outPath.getParent()); + } catch (IOException e) { + JOptionPane.showMessageDialog(parent, "Unable to create folder: " + outPath.getParent(), "Error", + JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + return false; + } + } + } } } + if (FileEx.create(outPath).exists()) { outPath = outPath.toAbsolutePath(); String options[] = { "Overwrite", "Cancel" }; @@ -1603,6 +1663,7 @@ private boolean saveResourceInternal(Resource resource, Component parent) { return false; } } + try (OutputStream os = StreamUtils.getOutputStream(outPath, true)) { ((Writeable) resource).write(os); } catch (IOException e) { @@ -1610,8 +1671,10 @@ private boolean saveResourceInternal(Resource resource, Component parent) { e.printStackTrace(); return false; } + JOptionPane.showMessageDialog(parent, "File saved to \"" + outPath.toAbsolutePath() + '\"', "Save complete", JOptionPane.INFORMATION_MESSAGE); + if ("IDS".equals(entry.getExtension())) { IdsMapCache.remove(entry); final IdsBrowser idsbrowser = ChildFrame.getFirstFrame(IdsBrowser.class); diff --git a/src/org/infinity/resource/are/viewer/AreaViewer.java b/src/org/infinity/resource/are/viewer/AreaViewer.java index c718f592c..b21d8a1fb 100644 --- a/src/org/infinity/resource/are/viewer/AreaViewer.java +++ b/src/org/infinity/resource/are/viewer/AreaViewer.java @@ -1640,7 +1640,7 @@ private void updateRegionTargets() { JCheckBox cb = cbLayers[LayerManager.getLayerTypeIndex(LayerType.REGION)]; cbLayerRegionTarget.setEnabled(cb.isSelected() && cb.isEnabled()); boolean state = cbLayerRegionTarget.isEnabled() && cbLayerRegionTarget.isSelected(); - layer.setIconLayerEnabled(state); + layer.setLayerEnabled(LayerRegion.LAYER_ICONS_TARGET, state); } else { cbLayerRegionTarget.setEnabled(false); } @@ -1659,7 +1659,7 @@ private void updateContainerTargets() { JCheckBox cb = cbLayers[LayerManager.getLayerTypeIndex(LayerType.CONTAINER)]; cbLayerContainerTarget.setEnabled(cb.isSelected() && cb.isEnabled()); boolean state = cbLayerContainerTarget.isEnabled() && cbLayerContainerTarget.isSelected(); - layer.setIconLayerEnabled(state); + layer.setLayerEnabled(LayerContainer.LAYER_ICONS_TARGET, state); } else { cbLayerContainerTarget.setEnabled(false); } @@ -1678,7 +1678,7 @@ private void updateDoorTargets() { JCheckBox cb = cbLayers[LayerManager.getLayerTypeIndex(LayerType.DOOR)]; cbLayerDoorTarget.setEnabled(cb.isSelected() && cb.isEnabled()); boolean state = cbLayerDoorTarget.isEnabled() && cbLayerDoorTarget.isSelected(); - layer.setIconLayerEnabled(state); + layer.setLayerEnabled(LayerDoor.LAYER_ICONS_TARGET, state); } else { cbLayerDoorTarget.setEnabled(false); } @@ -1840,6 +1840,7 @@ private void addLayerItem(LayerStackingType layer, LayerObject object) { type = ViewerConstants.LAYER_ITEM_POLY; break; case DOOR: + case DOOR_CELLS: type = ViewerConstants.DOOR_ANY | ViewerConstants.LAYER_ITEM_POLY; break; case CONTAINER_TARGET: @@ -1900,6 +1901,7 @@ private void removeLayerItem(LayerStackingType layer, LayerObject object) { type = ViewerConstants.LAYER_ITEM_POLY; break; case DOOR: + case DOOR_CELLS: type = ViewerConstants.DOOR_ANY | ViewerConstants.LAYER_ITEM_POLY; break; case CONTAINER_TARGET: @@ -1948,6 +1950,7 @@ private void orderLayerItems() { type = ViewerConstants.LAYER_ITEM_POLY; break; case DOOR: + case DOOR_CELLS: type = ViewerConstants.DOOR_ANY | ViewerConstants.LAYER_ITEM_POLY; break; case CONTAINER_TARGET: @@ -2136,9 +2139,12 @@ private void applySettings() { // applying animation active override settings ((LayerAnimation) layerManager.getLayer(LayerType.ANIMATION)) .setRealAnimationActiveIgnored(Settings.OverrideAnimVisibility); - ((LayerContainer) layerManager.getLayer(LayerType.CONTAINER)).setIconLayerEnabled(Settings.ShowContainerTargets); - ((LayerDoor) layerManager.getLayer(LayerType.DOOR)).setIconLayerEnabled(Settings.ShowDoorTargets); - ((LayerRegion) layerManager.getLayer(LayerType.REGION)).setIconLayerEnabled(Settings.ShowRegionTargets); + ((LayerContainer) layerManager.getLayer(LayerType.CONTAINER)).setLayerEnabled(LayerContainer.LAYER_ICONS_TARGET, + Settings.ShowContainerTargets); + ((LayerDoor) layerManager.getLayer(LayerType.DOOR)).setLayerEnabled(LayerDoor.LAYER_ICONS_TARGET, + Settings.ShowDoorTargets); + ((LayerRegion) layerManager.getLayer(LayerType.REGION)).setLayerEnabled(LayerRegion.LAYER_ICONS_TARGET, + Settings.ShowRegionTargets); // applying interpolation settings to animations switch (Settings.InterpolationAnim) { diff --git a/src/org/infinity/resource/are/viewer/BasicCompositeLayer.java b/src/org/infinity/resource/are/viewer/BasicCompositeLayer.java new file mode 100644 index 000000000..c4963cf10 --- /dev/null +++ b/src/org/infinity/resource/are/viewer/BasicCompositeLayer.java @@ -0,0 +1,57 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.are.viewer; + +import java.util.HashMap; + +import org.infinity.gui.layeritem.AbstractLayerItem; +import org.infinity.resource.AbstractStruct; +import org.infinity.resource.are.viewer.ViewerConstants.LayerType; + +/** + * Specialized base class for layer-specific managers that consists of multiple sublayers. + * + * @param Type of the layer item in the manager + * @param Type of the resource that contains layer items + */ +public abstract class BasicCompositeLayer extends BasicLayer { + /** Predefined identifier for use with the primary sublayer. */ + public static final int LAYER_PRIMARY = 0; + + private final HashMap layerEnabled = new HashMap<>(); + + public BasicCompositeLayer(R parent, LayerType type, AreaViewer viewer) { + super(parent, type, viewer); + setLayerEnabled(LAYER_PRIMARY, true); + } + + public boolean isLayerVisible(int id) { + return isLayerVisible() && layerEnabled.getOrDefault(id, false); + } + + @Override + public void setLayerVisible(boolean visible) { + setVisibilityState(visible); + + getLayerObjects().stream().forEach(obj -> { + AbstractLayerItem[] items = obj.getLayerItems(); + for (final AbstractLayerItem item : items) { + final int id = item.getId(); + item.setVisible(isLayerVisible(id) && isLayerEnabled(id)); + } + }); + } + + public boolean isLayerEnabled(int id) { + return layerEnabled.getOrDefault(id, false); + } + + public void setLayerEnabled(int id, boolean enable) { + if (isLayerEnabled(id) != enable) { + layerEnabled.put(id, enable); + setLayerVisible(isLayerVisible(LAYER_PRIMARY) || isLayerVisible(id)); + } + } +} diff --git a/src/org/infinity/resource/are/viewer/BasicTargetLayer.java b/src/org/infinity/resource/are/viewer/BasicTargetLayer.java deleted file mode 100644 index 09eeb3cc9..000000000 --- a/src/org/infinity/resource/are/viewer/BasicTargetLayer.java +++ /dev/null @@ -1,68 +0,0 @@ -// Near Infinity - An Infinity Engine Browser and Editor -// Copyright (C) 2001 Jon Olav Hauglid -// See LICENSE.txt for license information - -package org.infinity.resource.are.viewer; - -import org.infinity.gui.layeritem.AbstractLayerItem; -import org.infinity.resource.AbstractStruct; -import org.infinity.resource.are.viewer.ViewerConstants.LayerType; - -/** - * - */ -public abstract class BasicTargetLayer extends BasicLayer { - private boolean polyEnabled = true; - private boolean iconsEnabled; - - public BasicTargetLayer(R parent, LayerType type, AreaViewer viewer) { - super(parent, type, viewer); - } - - @Override - public void setLayerVisible(boolean visible) { - setVisibilityState(visible); - - getLayerObjects().stream().forEach(obj -> { - AbstractLayerItem[] items = obj.getLayerItems(ViewerConstants.LAYER_ITEM_POLY); - for (final AbstractLayerItem item : items) { - item.setVisible(isPolyLayerVisible() && isPolyLayerEnabled()); - } - - items = obj.getLayerItems(ViewerConstants.LAYER_ITEM_ICON); - for (final AbstractLayerItem item : items) { - item.setVisible(isIconsLayerVisible() && isIconsLayerEnabled()); - } - }); - } - - public boolean isPolyLayerVisible() { - return isLayerVisible() && polyEnabled; - } - - public boolean isIconsLayerVisible() { - return isLayerVisible() && iconsEnabled; - } - - public boolean isPolyLayerEnabled() { - return polyEnabled; - } - - public void setPolyLayerEnabled(boolean enable) { - if (enable != polyEnabled) { - polyEnabled = enable; - setLayerVisible(isPolyLayerVisible()); - } - } - - public boolean isIconsLayerEnabled() { - return iconsEnabled; - } - - public void setIconLayerEnabled(boolean enable) { - if (enable != iconsEnabled) { - iconsEnabled = enable; - setLayerVisible(isPolyLayerVisible() || isIconsLayerVisible()); - } - } -} diff --git a/src/org/infinity/resource/are/viewer/LayerActor.java b/src/org/infinity/resource/are/viewer/LayerActor.java index ae90bdbf7..7146e8a45 100644 --- a/src/org/infinity/resource/are/viewer/LayerActor.java +++ b/src/org/infinity/resource/are/viewer/LayerActor.java @@ -103,7 +103,7 @@ protected void loadLayer() { } } if (gamEntry == null) { - gamEntry = ResourceFactory.getResourceEntry(Profile.getProperty(Profile.Key.GET_GAM_NAME)); + gamEntry = ResourceFactory.getResourceEntry(Profile.getProperty(Profile.Key.GET_GAM_NAME)); } // scanning global NPCs diff --git a/src/org/infinity/resource/are/viewer/LayerContainer.java b/src/org/infinity/resource/are/viewer/LayerContainer.java index 10fb1f78c..3662b8b48 100644 --- a/src/org/infinity/resource/are/viewer/LayerContainer.java +++ b/src/org/infinity/resource/are/viewer/LayerContainer.java @@ -13,7 +13,10 @@ /** * Manages container layer objects. */ -public class LayerContainer extends BasicTargetLayer { +public class LayerContainer extends BasicCompositeLayer { + /** Identifier for the target icons sublayer. */ + public static final int LAYER_ICONS_TARGET = 1; + private static final String AVAILABLE_FMT = "Containers: %d"; public LayerContainer(AreResource are, AreaViewer viewer) { diff --git a/src/org/infinity/resource/are/viewer/LayerDoor.java b/src/org/infinity/resource/are/viewer/LayerDoor.java index 94b4dfa06..76785fd05 100644 --- a/src/org/infinity/resource/are/viewer/LayerDoor.java +++ b/src/org/infinity/resource/are/viewer/LayerDoor.java @@ -14,7 +14,10 @@ /** * Manages door layer objects. */ -public class LayerDoor extends BasicTargetLayer { +public class LayerDoor extends BasicCompositeLayer { + /** Identifier for the target icons sublayer. */ + public static final int LAYER_ICONS_TARGET = 1; + private static final String AVAILABLE_FMT = "Doors: %d"; private boolean doorClosed; @@ -42,17 +45,24 @@ public void setLayerVisible(boolean visible) { getLayerObjects().stream().forEach(obj -> { AbstractLayerItem[] items = obj.getLayerItems(ViewerConstants.DOOR_OPEN | ViewerConstants.LAYER_ITEM_POLY); for (final AbstractLayerItem item : items) { - item.setVisible(isPolyLayerVisible() && isPolyLayerEnabled() && !doorClosed); + item.setVisible(isLayerVisible(LAYER_PRIMARY) && isLayerEnabled(LAYER_PRIMARY) && !doorClosed); } items = obj.getLayerItems(ViewerConstants.DOOR_CLOSED | ViewerConstants.LAYER_ITEM_POLY); for (final AbstractLayerItem item : items) { - item.setVisible(isPolyLayerVisible() && isPolyLayerEnabled() && doorClosed); + item.setVisible(isLayerVisible(LAYER_PRIMARY) && isLayerEnabled(LAYER_PRIMARY) && doorClosed); } items = obj.getLayerItems(ViewerConstants.LAYER_ITEM_ICON); for (final AbstractLayerItem item : items) { - item.setVisible(isIconsLayerVisible() && isIconsLayerEnabled()); + switch (item.getId()) { + case LAYER_ICONS_TARGET: + item.setVisible(isLayerVisible(LAYER_ICONS_TARGET) && isLayerEnabled(LAYER_ICONS_TARGET)); + break; + default: + item.setVisible(false); + System.out.println("Unknown layer id: " + item.getId()); + } } }); } diff --git a/src/org/infinity/resource/are/viewer/LayerDoorCells.java b/src/org/infinity/resource/are/viewer/LayerDoorCells.java new file mode 100644 index 000000000..cc60979cd --- /dev/null +++ b/src/org/infinity/resource/are/viewer/LayerDoorCells.java @@ -0,0 +1,91 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.are.viewer; + +import static org.infinity.resource.are.AreResource.ARE_NUM_DOORS; +import static org.infinity.resource.are.AreResource.ARE_OFFSET_DOORS; + +import org.infinity.gui.layeritem.AbstractLayerItem; +import org.infinity.resource.are.AreResource; +import org.infinity.resource.are.Door; + +/** + * Manages blocked cells of door layer objects. + */ +public class LayerDoorCells extends BasicLayer { + private static final String AVAILABLE_FMT = "Doors: %d"; + + private boolean doorClosed; + + public LayerDoorCells(AreResource are, AreaViewer viewer) { + super(are, ViewerConstants.LayerType.DOOR_CELLS, viewer); + loadLayer(); + } + + @Override + protected void loadLayer() { + loadLayerItems(ARE_OFFSET_DOORS, ARE_NUM_DOORS, Door.class, d -> new LayerObjectDoorCells(parent, d)); + } + + @Override + public String getAvailability() { + int cnt = getLayerObjectCount(); + return String.format(AVAILABLE_FMT, cnt); + } + + @Override + public void setLayerVisible(boolean visible) { + setVisibilityState(visible); + +// getLayerObjects().stream().forEach(obj -> { +// AbstractLayerItem[] items = obj.getLayerItems(ViewerConstants.DOOR_OPEN | ViewerConstants.LAYER_ITEM_ICON); +// for (final AbstractLayerItem item : items) { +// item.setVisible(isLayerVisible() && !doorClosed); +// } +// +// items = obj.getLayerItems(ViewerConstants.DOOR_CLOSED | ViewerConstants.LAYER_ITEM_ICON); +// for (final AbstractLayerItem item : items) { +// item.setVisible(isLayerVisible() && doorClosed); +// } +// }); + + getLayerObjects().stream().forEach(obj -> { + AbstractLayerItem[] items = obj.getLayerItems(ViewerConstants.DOOR_OPEN | ViewerConstants.LAYER_ITEM_POLY); + for (final AbstractLayerItem item : items) { + item.setVisible(isLayerVisible() && !doorClosed); + } + + items = obj.getLayerItems(ViewerConstants.DOOR_CLOSED | ViewerConstants.LAYER_ITEM_POLY); + for (final AbstractLayerItem item : items) { + item.setVisible(isLayerVisible() && doorClosed); + } + }); + } + + /** + * Returns the current state of doors. + * + * @return Either {@code ViewerConstants.DOOR_OPEN} or {@code ViewerConstants.DOOR_CLOSED}. + */ + public int getDoorState() { + return doorClosed ? ViewerConstants.DOOR_CLOSED : ViewerConstants.DOOR_OPEN; + } + + /** + * Sets the state of doors for all door layer objects. + * + * @param state The door state (either {@code ViewerConstants.DOOR_OPEN} or {@code ViewerConstants.DOOR_CLOSED}). + */ + public void setDoorState(int state) { + boolean isClosed = (state == ViewerConstants.DOOR_CLOSED); + if (isClosed != doorClosed) { + doorClosed = isClosed; + if (isLayerVisible()) { + setLayerVisible(isLayerVisible()); + } + } + } + +} diff --git a/src/org/infinity/resource/are/viewer/LayerManager.java b/src/org/infinity/resource/are/viewer/LayerManager.java index f24bad590..b2a0dbf86 100644 --- a/src/org/infinity/resource/are/viewer/LayerManager.java +++ b/src/org/infinity/resource/are/viewer/LayerManager.java @@ -29,7 +29,8 @@ public final class LayerManager { LayerType.REGION, LayerType.TRANSITION, LayerType.DOOR_POLY, - LayerType.WALL_POLY + LayerType.WALL_POLY, + LayerType.DOOR_CELLS }; private final EnumMap> layers = new EnumMap<>(LayerType.class); @@ -360,6 +361,7 @@ public int getDoorState() { */ public void setDoorState(int state) { ((LayerDoor) getLayer(LayerType.DOOR)).setDoorState(state); + ((LayerDoorCells) getLayer(LayerType.DOOR_CELLS)).setDoorState(state); ((LayerDoorPoly) getLayer(LayerType.DOOR_POLY)).setDoorState(state); } @@ -586,6 +588,7 @@ private void init(AreResource are, WedResource wed, boolean forced) { case CONTAINER: case AMBIENT: case DOOR: + case DOOR_CELLS: case ANIMATION: case AUTOMAP: case SPAWN_POINT: @@ -669,6 +672,16 @@ private int loadLayer(LayerType layer, boolean forced) { } break; } + case DOOR_CELLS: { + if (layers.containsKey(layer)) { + retVal = layers.get(layer).loadLayer(forced); + } else { + LayerDoorCells obj = new LayerDoorCells(are, getViewer()); + layers.put(layer, obj); + retVal = obj.getLayerObjectCount(); + } + break; + } case ANIMATION: { if (layers.containsKey(layer)) { retVal = layers.get(layer).loadLayer(forced); diff --git a/src/org/infinity/resource/are/viewer/LayerObjectAreActor.java b/src/org/infinity/resource/are/viewer/LayerObjectAreActor.java index a01cd2325..da664290b 100644 --- a/src/org/infinity/resource/are/viewer/LayerObjectAreActor.java +++ b/src/org/infinity/resource/are/viewer/LayerObjectAreActor.java @@ -34,11 +34,13 @@ public class LayerObjectAreActor extends LayerObjectActor { static { ICONS.put(Allegiance.GOOD, new Image[] { ViewerIcons.ICON_ITM_ARE_ACTOR_G_1.getIcon().getImage(), - ViewerIcons.ICON_ITM_ARE_ACTOR_G_2.getIcon().getImage() }); + ViewerIcons.ICON_ITM_ARE_ACTOR_G_2.getIcon().getImage() }); + ICONS.put(Allegiance.NEUTRAL, new Image[] { ViewerIcons.ICON_ITM_ARE_ACTOR_B_1.getIcon().getImage(), - ViewerIcons.ICON_ITM_ARE_ACTOR_B_2.getIcon().getImage() }); + ViewerIcons.ICON_ITM_ARE_ACTOR_B_2.getIcon().getImage() }); + ICONS.put(Allegiance.ENEMY, new Image[] { ViewerIcons.ICON_ITM_ARE_ACTOR_R_1.getIcon().getImage(), - ViewerIcons.ICON_ITM_ARE_ACTOR_R_2.getIcon().getImage() }); + ViewerIcons.ICON_ITM_ARE_ACTOR_R_2.getIcon().getImage() }); } private final Actor actor; diff --git a/src/org/infinity/resource/are/viewer/LayerObjectAutomap.java b/src/org/infinity/resource/are/viewer/LayerObjectAutomap.java index e9a32ad85..e17606e29 100644 --- a/src/org/infinity/resource/are/viewer/LayerObjectAutomap.java +++ b/src/org/infinity/resource/are/viewer/LayerObjectAutomap.java @@ -11,19 +11,12 @@ import org.infinity.datatype.IsNumeric; import org.infinity.gui.layeritem.AbstractLayerItem; import org.infinity.gui.layeritem.IconLayerItem; -import org.infinity.resource.Profile; import org.infinity.resource.Viewable; import org.infinity.resource.are.AreResource; import org.infinity.resource.are.AutomapNote; import org.infinity.resource.are.viewer.icon.ViewerIcons; import org.infinity.resource.key.FileResourceEntry; -import org.infinity.resource.to.StrRefEntry; -import org.infinity.resource.to.StrRefEntry2; -import org.infinity.resource.to.StringEntry; -import org.infinity.resource.to.StringEntry2; import org.infinity.resource.to.TohResource; -import org.infinity.resource.to.TotResource; -import org.infinity.util.io.FileEx; import org.infinity.util.io.FileManager; /** @@ -52,65 +45,17 @@ public LayerObjectAutomap(AreResource parent, AutomapNote note) { msg = note.getAttribute(AutomapNote.ARE_AUTOMAP_TEXT).toString(); } else { // fetching string from talk override - msg = "[user-defined]"; int srcStrref = ((IsNumeric) note.getAttribute(AutomapNote.ARE_AUTOMAP_TEXT)).getValue(); + msg = String.format("[Overridden string (Strref: %d)]", srcStrref); if (srcStrref > 0) { String path = parent.getResourceEntry().getActualPath().toString(); path = path.replace(parent.getResourceEntry().getResourceName(), ""); - if (Profile.isEnhancedEdition()) { - // processing new TOH structure - Path tohFile = FileManager.resolve(path, "DEFAULT.TOH"); - if (FileEx.create(tohFile).exists()) { - FileResourceEntry tohEntry = new FileResourceEntry(tohFile); - TohResource toh = new TohResource(tohEntry); - IsNumeric so = (IsNumeric) toh.getAttribute(TohResource.TOH_OFFSET_ENTRIES); - IsNumeric sc = (IsNumeric) toh.getAttribute(TohResource.TOH_NUM_ENTRIES); - if (so != null && sc != null && sc.getValue() > 0) { - for (int i = 0, count = sc.getValue(), curOfs = so.getValue(); i < count; i++) { - StrRefEntry2 strref = (StrRefEntry2) toh.getAttribute(curOfs, false); - if (strref != null) { - int v = ((IsNumeric) strref.getAttribute(StrRefEntry2.TOH_STRREF_OVERRIDDEN)).getValue(); - if (v == srcStrref) { - int sofs = ((IsNumeric) strref.getAttribute(StrRefEntry2.TOH_STRREF_OFFSET_STRING)).getValue(); - StringEntry2 se = (StringEntry2) toh.getAttribute(so.getValue() + sofs, false); - if (se != null) { - msg = se.getAttribute(StringEntry2.TOH_STRING_TEXT).toString(); - } - break; - } - curOfs += strref.getSize(); - } - } - } - } - } else { - // processing legacy TOH/TOT structures - Path tohFile = FileManager.resolve(path, "DEFAULT.TOH"); - Path totFile = FileManager.resolve(path, "DEFAULT.TOT"); - if (FileEx.create(tohFile).exists() && FileEx.create(totFile).exists()) { - FileResourceEntry tohEntry = new FileResourceEntry(tohFile); - FileResourceEntry totEntry = new FileResourceEntry(totFile); - TohResource toh = new TohResource(tohEntry); - TotResource tot = new TotResource(totEntry); - IsNumeric sc = (IsNumeric) toh.getAttribute(TohResource.TOH_NUM_ENTRIES); - if (sc != null && sc.getValue() > 0) { - for (int i = 0, count = sc.getValue(), curOfs = 0x14; i < count; i++) { - StrRefEntry strref = (StrRefEntry) toh.getAttribute(curOfs, false); - if (strref != null) { - int v = ((IsNumeric) strref.getAttribute(StrRefEntry.TOH_STRREF_OVERRIDDEN)).getValue(); - if (v == srcStrref) { - int sofs = ((IsNumeric) strref.getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING)).getValue(); - StringEntry se = (StringEntry) tot.getAttribute(sofs, false); - if (se != null) { - msg = se.getAttribute(StringEntry.TOT_STRING_TEXT).toString(); - } - break; - } - curOfs += strref.getSize(); - } - } - } - } + final Path tohFile = FileManager.resolve(path, "DEFAULT.TOH"); + final Path totFile = FileManager.resolve(path, "DEFAULT.TOT"); + final String result = TohResource.getOverrideString(new FileResourceEntry(tohFile), + new FileResourceEntry(totFile), srcStrref); + if (result != null) { + msg = result; } } } diff --git a/src/org/infinity/resource/are/viewer/LayerObjectContainer.java b/src/org/infinity/resource/are/viewer/LayerObjectContainer.java index f3bc49963..1bb11059c 100644 --- a/src/org/infinity/resource/are/viewer/LayerObjectContainer.java +++ b/src/org/infinity/resource/are/viewer/LayerObjectContainer.java @@ -190,7 +190,7 @@ private IconLayerItem createValidatedLayerItem(Point pt, String label, Image[] i IconLayerItem retVal = null; if (pt.x > 0 && pt.y > 0) { - retVal = new IconLayerItem(container, label, icons[0], CENTER); + retVal = new IconLayerItem(container, label, LayerContainer.LAYER_ICONS_TARGET, icons[0], CENTER); retVal.setLabelEnabled(Settings.ShowLabelContainerTargets); retVal.setName(getCategory()); retVal.setImage(AbstractLayerItem.ItemState.HIGHLIGHTED, icons[1]); diff --git a/src/org/infinity/resource/are/viewer/LayerObjectDoor.java b/src/org/infinity/resource/are/viewer/LayerObjectDoor.java index 838859dbd..aaee4035c 100644 --- a/src/org/infinity/resource/are/viewer/LayerObjectDoor.java +++ b/src/org/infinity/resource/are/viewer/LayerObjectDoor.java @@ -65,10 +65,10 @@ public LayerObjectDoor(AreResource parent, Door door) { doorMap.put(Integer.valueOf(ViewerConstants.DOOR_OPEN), doorOpen); final DoorInfo doorClosed = new DoorInfo(); doorMap.put(Integer.valueOf(ViewerConstants.DOOR_CLOSED), doorClosed); - String label = null; + String name = null; try { - String attr = getAttributes(); - final String name = door.getAttribute(Door.ARE_DOOR_NAME).toString(); + String attr = getAttributes(this.door); + name = door.getAttribute(Door.ARE_DOOR_NAME).toString(); final int vOfs = ((IsNumeric) parent.getAttribute(AreResource.ARE_OFFSET_VERTICES)).getValue(); // processing opened state door @@ -81,7 +81,6 @@ public LayerObjectDoor(AreResource parent, Door door) { vNum = ((IsNumeric) door.getAttribute(Door.ARE_DOOR_NUM_VERTICES_CLOSED)).getValue(); doorClosed.setCoords(loadVertices(door, vOfs, 0, vNum, ClosedVertex.class)); - label = door.getAttribute(Door.ARE_DOOR_NAME).toString(); closePoint.x = ((IsNumeric) door.getAttribute(Door.ARE_DOOR_LOCATION_CLOSE_X)).getValue(); closePoint.y = ((IsNumeric) door.getAttribute(Door.ARE_DOOR_LOCATION_CLOSE_Y)).getValue(); openPoint.x = ((IsNumeric) door.getAttribute(Door.ARE_DOOR_LOCATION_OPEN_X)).getValue(); @@ -109,9 +108,9 @@ public LayerObjectDoor(AreResource parent, Door door) { info.setItem(item); } - itemIconClose = createValidatedLayerItem(closePoint, label, getIcons(ICONS_CLOSE)); - itemIconOpen = createValidatedLayerItem(openPoint, label, getIcons(ICONS_OPEN)); - itemIconLaunch = createValidatedLayerItem(launchPoint, label, getIcons(ICONS_LAUNCH)); + itemIconClose = createValidatedLayerItem(closePoint, name, getIcons(ICONS_CLOSE)); + itemIconOpen = createValidatedLayerItem(openPoint, name, getIcons(ICONS_OPEN)); + itemIconLaunch = createValidatedLayerItem(launchPoint, name, getIcons(ICONS_LAUNCH)); } @Override @@ -218,7 +217,10 @@ public void update(double zoomFactor) { } } - private String getAttributes() { + /** + * Returns a descriptive string for the specified {@link Door} structure. + */ + public static String getAttributes(Door door) { final StringBuilder sb = new StringBuilder(); sb.append('['); @@ -256,7 +258,7 @@ private IconLayerItem createValidatedLayerItem(Point pt, String label, Image[] i IconLayerItem retVal = null; if (pt.x > 0 && pt.y > 0) { - retVal = new IconLayerItem(door, label, icons[0], CENTER); + retVal = new IconLayerItem(door, label, LayerDoor.LAYER_ICONS_TARGET, icons[0], CENTER); retVal.setLabelEnabled(Settings.ShowLabelDoorTargets); retVal.setName(getCategory()); retVal.setImage(AbstractLayerItem.ItemState.HIGHLIGHTED, icons[1]); diff --git a/src/org/infinity/resource/are/viewer/LayerObjectDoorCells.java b/src/org/infinity/resource/are/viewer/LayerObjectDoorCells.java new file mode 100644 index 000000000..a94971112 --- /dev/null +++ b/src/org/infinity/resource/are/viewer/LayerObjectDoorCells.java @@ -0,0 +1,263 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.are.viewer; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import org.infinity.datatype.IsNumeric; +import org.infinity.gui.layeritem.AbstractLayerItem; +import org.infinity.gui.layeritem.ShapedLayerItem; +import org.infinity.resource.Profile; +import org.infinity.resource.Viewable; +import org.infinity.resource.are.AreResource; +import org.infinity.resource.are.Door; +import org.infinity.resource.vertex.ClosedVertexImpeded; +import org.infinity.resource.vertex.OpenVertexImpeded; + +/** + * Handles specific layer subtype: ARE/Door blocked cells + */ +public class LayerObjectDoorCells extends LayerObject { + private static final Color[] COLOR = { new Color(0xFF004000, true), new Color(0xFF004000, true), + new Color(0xC000C000, true), new Color(0xC000FF00, true) }; + + private final HashMap doorMap = new HashMap<>(4); + private final Door door; + + /** + * @param category + * @param classType + * @param parent + */ + public LayerObjectDoorCells(AreResource parent, Door door) { + super("Door", Door.class, parent); + this.door = door; + final DoorInfo doorOpen = new DoorInfo(); + doorMap.put(Integer.valueOf(ViewerConstants.DOOR_OPEN), doorOpen); + final DoorInfo doorClosed = new DoorInfo(); + doorMap.put(Integer.valueOf(ViewerConstants.DOOR_CLOSED), doorClosed); + try { + String attr = LayerObjectDoor.getAttributes(this.door); + final String name = door.getAttribute(Door.ARE_DOOR_NAME).toString(); + final int cOfs = ((IsNumeric) parent.getAttribute(AreResource.ARE_OFFSET_VERTICES)).getValue(); + + // processing opened state door cells + doorOpen.setMessage(String.format("%s (Open) %s", name, attr)); + int cNum = ((IsNumeric) door.getAttribute(Door.ARE_DOOR_NUM_VERTICES_IMPEDED_OPEN)).getValue(); + Point[][] itemCoords = createCellPolygons(loadVertices(door, cOfs, 0, cNum, OpenVertexImpeded.class)); + doorOpen.setCoords(itemCoords); + + // processing closed state door cells + doorClosed.setMessage(String.format("%s (Closed) %s", name, attr)); + cNum = ((IsNumeric) door.getAttribute(Door.ARE_DOOR_NUM_VERTICES_IMPEDED_CLOSED)).getValue(); + itemCoords = createCellPolygons(loadVertices(door, cOfs, 0, cNum, ClosedVertexImpeded.class)); + doorClosed.setCoords(itemCoords); + } catch (Exception e) { + e.printStackTrace(); + } + + for (final DoorInfo info: getDoors()) { + final Point[][] coords = info.getCoords(); + final ShapedLayerItem[] items = new ShapedLayerItem[coords.length]; + final Point[] locations = new Point[coords.length]; + for (int i = 0; i < coords.length; i++) { + final Polygon poly = createPolygon(coords[i], 1.0); + final Rectangle bounds = normalizePolygon(poly); + + locations[i] = new Point(bounds.x, bounds.y); + final ShapedLayerItem cell = new ShapedLayerItem(this.door, info.getMessage(), poly); + cell.setName(getCategory()); + cell.setStrokeColor(AbstractLayerItem.ItemState.NORMAL, COLOR[0]); + cell.setStrokeColor(AbstractLayerItem.ItemState.HIGHLIGHTED, COLOR[1]); + cell.setFillColor(AbstractLayerItem.ItemState.NORMAL, COLOR[2]); + cell.setFillColor(AbstractLayerItem.ItemState.HIGHLIGHTED, COLOR[3]); + cell.setStroked(false); + cell.setFilled(true); + cell.setVisible(isVisible()); + items[i] = cell; + } + info.setCellItems(items); + info.setLocations(locations); + } + } + + @Override + public Viewable getViewable() { + return door; + } + + @Override + public AbstractLayerItem[] getLayerItems(int type) { + boolean isClosed = (type & ViewerConstants.DOOR_CLOSED) == ViewerConstants.DOOR_CLOSED; + boolean isOpen = (type & ViewerConstants.DOOR_OPEN) == ViewerConstants.DOOR_OPEN; + + if (Profile.getEngine() == Profile.Engine.PST) { + // open/closed states are inverted for PST + boolean tmp = isClosed; + isClosed = isOpen; + isOpen = tmp; + } + + List list = new ArrayList<>(); + if (isOpen) { + final DoorInfo info = getDoor(ViewerConstants.DOOR_OPEN); + if (info != null && info.getCellItems() != null) { + final ShapedLayerItem[] items = info.getCellItems(); + for (int i = 0; i < items.length; i++) { + list.add(items[i]); + } + } + } + if (isClosed) { + final DoorInfo info = getDoor(ViewerConstants.DOOR_CLOSED); + if (info != null && info.getCellItems() != null) { + final ShapedLayerItem[] items = info.getCellItems(); + for (int i = 0; i < items.length; i++) { + list.add(items[i]); + } + } + } + + return list.toArray(new AbstractLayerItem[0]); + } + + @Override + public AbstractLayerItem[] getLayerItems() { + List list = new ArrayList<>(); + + for (final AbstractLayerItem[] items : getDoorItems()) { + if (items != null) { + for (int i = 0; i < items.length; i++) { + list.add(items[i]); + } + } + } + + return list.toArray(new AbstractLayerItem[0]); + } + + @Override + public void update(double zoomFactor) { + for (final DoorInfo info : getDoors()) { + final ShapedLayerItem[] items = info.getCellItems(); + final Point[] locations = info.getLocations(); + final Point[][] coords = info.getCoords(); + + for (int i = 0; i < items.length; i++) { + items[i].setItemLocation((int) (locations[i].x * zoomFactor + (zoomFactor / 2.0)), + (int) (locations[i].y * zoomFactor + (zoomFactor / 2.0))); + final Polygon poly = createPolygon(coords[i], zoomFactor); + normalizePolygon(poly); + items[i].setShape(poly); + } + } + } + + private Collection getDoors() { + return doorMap.values(); + } + + private Collection getDoorItems() { + return doorMap.values().stream().map(ci -> ci.getCellItems()).filter(cell -> cell != null).collect(Collectors.toList()); + } + + private DoorInfo getDoor(int id) { + return doorMap.get(Integer.valueOf(id)); + } + + /** + * Creates polygons out of the given cell block coordinates and returns them as arrays of Point objects (one array per + * polygon). + * + * @param cells Array of search map coordinates for blocked cells. + * @return Two-dimensional array of polygon points. First array dimension indicates the polygon, second dimension + * indicates coordinates for the polygon. + */ + private Point[][] createCellPolygons(Point[] cells) { + List polys = new ArrayList<>(); + + // add one polygon per cell + // TODO: combine as many cells as possible into one polygon to improve performance + if (cells != null && cells.length > 0) { + final int cw = 16; // search map cell width, in pixels + final int ch = 12; // search map cell height, in pixels + for (int i = 0; i < cells.length; i++) { + final int x = cells[i].x * cw; + final int y = cells[i].y * ch; + polys.add(new Point[] { new Point(x, y), new Point(x + cw, y), new Point(x + cw, y + ch), new Point(x, y + ch) }); + } + } + + Point[][] retVal = new Point[polys.size()][]; + for (int i = 0, size = polys.size(); i < size; i++) { + retVal[i] = polys.get(i); + } + return retVal; + } + + // ----------------------------- INNER CLASSES ----------------------------- + + /** Storage for open/close-based door cell information. */ + private static class DoorInfo { + private String message; + private ShapedLayerItem[] cells; + private Point[] locations; + private Point[][] coords; + + public DoorInfo() {} + + /** Returns a message or label associated with the door. */ + public String getMessage() { + return message; + } + + /** Defines a message or label associated with the door. */ + public DoorInfo setMessage(String msg) { + this.message = msg; + return this; + } + + /** Returns the layer for blocked door cells. */ + public ShapedLayerItem[] getCellItems() { + return this.cells; + } + + /** Defines the layer items for blocked door cells. */ + public DoorInfo setCellItems(ShapedLayerItem[] cells) { + this.cells = cells; + return this; + } + + /** Returns the origin of all blocked door cells. */ + public Point[] getLocations() { + return this.locations; + } + + /** Defines the origin of all blocked door cells. */ + public DoorInfo setLocations(Point[] locations) { + this.locations = locations; + return this; + } + + /** Returns the vertices for all blocked door cells. */ + public Point[][] getCoords() { + return this.coords; + } + + /** Defines the vertices for all blocked door cells. */ + public DoorInfo setCoords(Point[][] coords) { + this.coords = coords; + return this; + } + } +} diff --git a/src/org/infinity/resource/are/viewer/LayerObjectRegion.java b/src/org/infinity/resource/are/viewer/LayerObjectRegion.java index bb9d5ed52..64ad68065 100644 --- a/src/org/infinity/resource/are/viewer/LayerObjectRegion.java +++ b/src/org/infinity/resource/are/viewer/LayerObjectRegion.java @@ -246,7 +246,7 @@ private IconLayerItem createValidatedLayerItem(Point pt, String label, Image[] i IconLayerItem retVal = null; if (pt.x > 0 && pt.y > 0) { - retVal = new IconLayerItem(region, label, icons[0], CENTER); + retVal = new IconLayerItem(region, label, LayerRegion.LAYER_ICONS_TARGET, icons[0], CENTER); retVal.setLabelEnabled(Settings.ShowLabelRegionTargets); retVal.setName(getCategory()); retVal.setImage(AbstractLayerItem.ItemState.HIGHLIGHTED, icons[1]); diff --git a/src/org/infinity/resource/are/viewer/LayerRegion.java b/src/org/infinity/resource/are/viewer/LayerRegion.java index a32f0d4f8..1a3902153 100644 --- a/src/org/infinity/resource/are/viewer/LayerRegion.java +++ b/src/org/infinity/resource/are/viewer/LayerRegion.java @@ -13,7 +13,10 @@ /** * Manages region layer objects. */ -public class LayerRegion extends BasicTargetLayer { +public class LayerRegion extends BasicCompositeLayer { + /** Identifier for the target icons sublayer. */ + public static final int LAYER_ICONS_TARGET = 1; + private static final String AVAILABLE_FMT = "Regions: %d"; public LayerRegion(AreResource are, AreaViewer viewer) { diff --git a/src/org/infinity/resource/are/viewer/Settings.java b/src/org/infinity/resource/are/viewer/Settings.java index 254a48613..1aedb4f6b 100644 --- a/src/org/infinity/resource/are/viewer/Settings.java +++ b/src/org/infinity/resource/are/viewer/Settings.java @@ -34,6 +34,7 @@ public class Settings { ViewerConstants.LayerStackingType.DOOR, ViewerConstants.LayerStackingType.DOOR_POLY, ViewerConstants.LayerStackingType.WALL_POLY, + ViewerConstants.LayerStackingType.DOOR_CELLS, ViewerConstants.LayerStackingType.AMBIENT_RANGE, ViewerConstants.LayerStackingType.TRANSITION }; @@ -566,6 +567,8 @@ public static LayerType stackingToLayer(LayerStackingType type) { case DOOR: case DOOR_TARGET: return LayerType.DOOR; + case DOOR_CELLS: + return LayerType.DOOR_CELLS; case DOOR_POLY: return LayerType.DOOR_POLY; case ENTRANCE: @@ -601,6 +604,8 @@ public static LayerStackingType[] layerToStacking(LayerType type) { return new LayerStackingType[] { LayerStackingType.CONTAINER, LayerStackingType.CONTAINER_TARGET }; case DOOR: return new LayerStackingType[] { LayerStackingType.DOOR, LayerStackingType.DOOR_TARGET }; + case DOOR_CELLS: + return new LayerStackingType[] { LayerStackingType.DOOR_CELLS }; case DOOR_POLY: return new LayerStackingType[] { LayerStackingType.DOOR_POLY }; case ENTRANCE: diff --git a/src/org/infinity/resource/are/viewer/ViewerConstants.java b/src/org/infinity/resource/are/viewer/ViewerConstants.java index 8f34b92b9..b5f52246d 100644 --- a/src/org/infinity/resource/are/viewer/ViewerConstants.java +++ b/src/org/infinity/resource/are/viewer/ViewerConstants.java @@ -20,6 +20,7 @@ public static enum LayerType { CONTAINER("Containers", true), AMBIENT("Ambient Sounds", true), DOOR("Doors", true), + DOOR_CELLS("Impeded Door Cells", true), ANIMATION("Background Animations", true), AUTOMAP("Automap Notes", true), SPAWN_POINT("Spawn Points", true), @@ -64,6 +65,7 @@ public static enum LayerStackingType { AMBIENT("Ambient Sounds"), AMBIENT_RANGE("Ambient Sound Ranges"), DOOR("Doors"), + DOOR_CELLS("Impeded Door Cells"), ANIMATION("Background Animations"), AUTOMAP("Automap Notes"), SPAWN_POINT("Spawn Points"), diff --git a/src/org/infinity/resource/bcs/BafResource.java b/src/org/infinity/resource/bcs/BafResource.java index d3b0fc962..a11c32527 100644 --- a/src/org/infinity/resource/bcs/BafResource.java +++ b/src/org/infinity/resource/bcs/BafResource.java @@ -104,7 +104,9 @@ public void actionPerformed(ActionEvent event) { } else if (bpCode.getControlByType(CTRL_DECOMPILE) == event.getSource()) { decompile(); } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE) == event.getSource()) { - save(); + save(false); + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == event.getSource()) { + save(true); } else if (buttonPanel.getControlByType(CTRL_SAVE_SCRIPT) == event.getSource()) { saveScript(); } else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) { @@ -133,6 +135,7 @@ public void insertUpdate(DocumentEvent event) { bpCode.getControlByType(CTRL_DECOMPILE).setEnabled(true); } else if (event.getDocument() == sourceText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(true); bpSource.getControlByType(CTRL_COMPILE).setEnabled(true); sourceChanged = true; } @@ -144,6 +147,7 @@ public void removeUpdate(DocumentEvent event) { bpCode.getControlByType(CTRL_DECOMPILE).setEnabled(true); } else if (event.getDocument() == sourceText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(true); bpSource.getControlByType(CTRL_COMPILE).setEnabled(true); sourceChanged = true; } @@ -155,6 +159,7 @@ public void changedUpdate(DocumentEvent event) { bpCode.getControlByType(CTRL_DECOMPILE).setEnabled(true); } else if (event.getDocument() == sourceText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(true); bpSource.getControlByType(CTRL_COMPILE).setEnabled(true); sourceChanged = true; } @@ -309,6 +314,8 @@ public JComponent makeViewer(ViewableContainer container) { ((JButton) buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this); JButton bSave = (JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE); bSave.addActionListener(this); + JButton bSaveAs = (JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS); + bSaveAs.addActionListener(this); JButton bSaveScript = new JButton("Save code", Icons.ICON_SAVE_16.getIcon()); bSaveScript.addActionListener(this); buttonPanel.addControl(bSaveScript, CTRL_SAVE_SCRIPT); @@ -327,6 +334,7 @@ public JComponent makeViewer(ViewableContainer container) { bpmWarnings.setEnabled(false); bDecompile.setEnabled(false); bSave.setEnabled(false); + bSaveAs.setEnabled(false); bpmUses.setEnabled(false); bSaveScript.setEnabled(false); @@ -441,9 +449,10 @@ private void decompile() { tabbedPane.setSelectedIndex(0); } - private void save() { - JButton bSave = (JButton) buttonPanel.getControlByType(ButtonPanel.Control.SAVE); - ButtonPopupMenu bpmErrors = (ButtonPopupMenu) bpSource.getControlByType(CTRL_ERRORS); + private void save(boolean interactive) { + final JButton bSave = (JButton) buttonPanel.getControlByType(ButtonPanel.Control.SAVE); + final JButton bSaveAs = (JButton) buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS); + final ButtonPopupMenu bpmErrors = (ButtonPopupMenu) bpSource.getControlByType(CTRL_ERRORS); if (bpmErrors.isEnabled()) { String options[] = { "Save", "Cancel" }; int result = JOptionPane.showOptionDialog(panel, "Script contains errors. Save anyway?", "Errors found", @@ -452,8 +461,15 @@ private void save() { return; } } - if (ResourceFactory.saveResource(this, panel.getTopLevelAncestor())) { + final boolean result; + if (interactive) { + result = ResourceFactory.saveResourceAs(this, panel.getTopLevelAncestor()); + } else { + result = ResourceFactory.saveResource(this, panel.getTopLevelAncestor()); + } + if (result) { bSave.setEnabled(false); + bSaveAs.setEnabled(false); sourceChanged = false; } } diff --git a/src/org/infinity/resource/bcs/BcsResource.java b/src/org/infinity/resource/bcs/BcsResource.java index b51c2bba8..2d39ef827 100644 --- a/src/org/infinity/resource/bcs/BcsResource.java +++ b/src/org/infinity/resource/bcs/BcsResource.java @@ -339,7 +339,9 @@ public void actionPerformed(ActionEvent event) { } else if (bpCompiled.getControlByType(CTRL_DECOMPILE) == event.getSource()) { decompile(); } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE) == event.getSource()) { - save(); + save(false); + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == event.getSource()) { + save(true); } } @@ -389,6 +391,7 @@ public void searchReferences(Component parent) { public void insertUpdate(DocumentEvent event) { if (event.getDocument() == codeText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(true); bpCompiled.getControlByType(CTRL_DECOMPILE).setEnabled(true); sourceChanged = false; codeChanged = true; @@ -402,6 +405,7 @@ public void insertUpdate(DocumentEvent event) { public void removeUpdate(DocumentEvent event) { if (event.getDocument() == codeText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(true); bpCompiled.getControlByType(CTRL_DECOMPILE).setEnabled(true); sourceChanged = false; codeChanged = true; @@ -415,6 +419,7 @@ public void removeUpdate(DocumentEvent event) { public void changedUpdate(DocumentEvent event) { if (event.getDocument() == codeText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(true); bpCompiled.getControlByType(CTRL_DECOMPILE).setEnabled(true); sourceChanged = false; codeChanged = true; @@ -628,7 +633,8 @@ public JComponent makeViewer(ViewableContainer container) { bpmExport.addItemListener(this); JButton bSave = (JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE); bSave.addActionListener(this); - bSave.setEnabled(false); + JButton bSaveAs = (JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS); + bSaveAs.addActionListener(this); tabbedPane = new JTabbedPane(); tabbedPane.addTab("Script source (decompiled)", decompiledPanel); @@ -650,6 +656,7 @@ public JComponent makeViewer(ViewableContainer container) { } bDecompile.setEnabled(false); bSave.setEnabled(false); + bSaveAs.setEnabled(false); return panel; } @@ -754,9 +761,10 @@ private void decompile() { tabbedPane.setSelectedIndex(0); } - private void save() { - JButton bSave = (JButton) buttonPanel.getControlByType(ButtonPanel.Control.SAVE); - ButtonPopupMenu bpmErrors = (ButtonPopupMenu) bpDecompile.getControlByType(CTRL_ERRORS); + private void save(boolean interactive) { + final JButton bSave = (JButton) buttonPanel.getControlByType(ButtonPanel.Control.SAVE); + final JButton bSaveAs = (JButton) buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS); + final ButtonPopupMenu bpmErrors = (ButtonPopupMenu) bpDecompile.getControlByType(CTRL_ERRORS); if (bpmErrors.isEnabled()) { String options[] = { "Save", "Cancel" }; int result = JOptionPane.showOptionDialog(panel, "Script contains errors. Save anyway?", "Errors found", @@ -765,8 +773,15 @@ private void save() { return; } } - if (ResourceFactory.saveResource(this, panel.getTopLevelAncestor())) { + final boolean result; + if (interactive) { + result = ResourceFactory.saveResourceAs(this, panel.getTopLevelAncestor()); + } else { + result = ResourceFactory.saveResource(this, panel.getTopLevelAncestor()); + } + if (result) { bSave.setEnabled(false); + bSaveAs.setEnabled(false); sourceChanged = false; codeChanged = false; } diff --git a/src/org/infinity/resource/cre/CreResource.java b/src/org/infinity/resource/cre/CreResource.java index 7126acb7a..fbd8fd580 100644 --- a/src/org/infinity/resource/cre/CreResource.java +++ b/src/org/infinity/resource/cre/CreResource.java @@ -766,6 +766,64 @@ public static String getScriptName(ByteBuffer buffer) throws IOException { return retVal; } + /** + * Removes characters from flag descriptions which are shared by all relevant ids entries. + * + * @param field The {@link IdsFlag} instance to adjust. + * @param idsFile Associated IDS file. + * @param separatorChar Character to separate individual words (usually {@code '_'}). + * @return Updated {@link IdsFlag} instance. + */ + public static IdsFlag uniqueIdsFlag(IdsFlag field, String idsFile, char separatorChar) { + if (field == null) { + return field; + } + IdsMap map = IdsMapCache.get(idsFile); + if (map == null) { + return field; + } + + String[] table = new String[field.getSize() * 8]; + // determine longest common prefix + IdsMapEntry entry = map.get(0L); + String prefix = (entry != null) ? entry.getSymbol() : null; + for (int i = 0; i < table.length; i++) { + entry = map.get(1L << i); + if (entry != null) { + if (prefix == null) { + prefix = entry.getSymbol(); + } else { + String name = entry.getSymbol(); + for (int j = 0, jmax = Math.min(prefix.length(), name.length()); j < jmax; j++) { + if (Character.toUpperCase(prefix.charAt(j)) != Character.toUpperCase(name.charAt(j))) { + prefix = prefix.substring(0, j); + break; + } + } + } + } + } + if (prefix == null) { + prefix = ""; + } + + // cut off prefix after last matching separator character + if (separatorChar != 0) { + prefix = prefix.substring(0, prefix.lastIndexOf(separatorChar) + 1); + } + + // update flag descriptions + entry = map.get(0L); + field.setEmptyDesc((entry != null) ? entry.getSymbol().substring(prefix.length()) : null); + for (int i = 0; i < table.length; i++) { + entry = map.get(1L << i); + table[i] = (entry != null) ? entry.getSymbol().substring(prefix.length()) : null; + } + field.setFlagDescriptions(field.getSize(), table, 0); + + return field; + } + public CreResource(String name) throws Exception { super(null, name, StructureFactory.getInstance().createStructure(StructureFactory.ResType.RES_CRE, null, null).getBuffer(), 0); @@ -2036,57 +2094,6 @@ private int setColorFieldsPSTEE(int animId, ByteBuffer buffer, int startOffset, return startOffset; } - // Removes characters from flag descriptions which are shared by all relevant ids entries. - private IdsFlag uniqueIdsFlag(IdsFlag field, String idsFile, char separatorChar) { - if (field == null) { - return field; - } - IdsMap map = IdsMapCache.get(idsFile); - if (map == null) { - return field; - } - - String[] table = new String[field.getSize() * 8]; - // determine longest common prefix - IdsMapEntry entry = map.get(0L); - String prefix = (entry != null) ? entry.getSymbol() : null; - for (int i = 0; i < table.length; i++) { - entry = map.get(1L << i); - if (entry != null) { - if (prefix == null) { - prefix = entry.getSymbol(); - } else { - String name = entry.getSymbol(); - for (int j = 0, jmax = Math.min(prefix.length(), name.length()); j < jmax; j++) { - if (Character.toUpperCase(prefix.charAt(j)) != Character.toUpperCase(name.charAt(j))) { - prefix = prefix.substring(0, j); - break; - } - } - } - } - } - if (prefix == null) { - prefix = ""; - } - - // cut off prefix after last matching separator character - if (separatorChar != 0) { - prefix = prefix.substring(0, prefix.lastIndexOf(separatorChar) + 1); - } - - // update flag descriptions - entry = map.get(0L); - field.setEmptyDesc((entry != null) ? entry.getSymbol().substring(prefix.length()) : null); - for (int i = 0; i < table.length; i++) { - entry = map.get(1L << i); - table[i] = (entry != null) ? entry.getSymbol().substring(prefix.length()) : null; - } - field.setFlagDescriptions(field.getSize(), table, 0); - - return field; - } - /** * Converts effect structures to the specified EFF version. * diff --git a/src/org/infinity/resource/effects/BaseOpcode.java b/src/org/infinity/resource/effects/BaseOpcode.java index c3c6fc971..28de51e10 100644 --- a/src/org/infinity/resource/effects/BaseOpcode.java +++ b/src/org/infinity/resource/effects/BaseOpcode.java @@ -983,16 +983,6 @@ public static int makeEffectStruct(int id, Datatype parent, ByteBuffer buffer, i return getOpcode(id).makeEffectStruct(parent, buffer, offset, list, isVersion1); } - /** Returns whether the current game is enhanced by TobEx. */ - protected static boolean isTobEx() { - return Profile.getProperty(Profile.Key.IS_GAME_TOBEX); - } - - /** Returns whether the current game is enhanced by EEEx. */ - protected static boolean isEEEx() { - return Profile.getProperty(Profile.Key.IS_GAME_EEEX); - } - /** * Returns the specified opcode instance. Returns a default instance if the requested opcode doesn't exist. * @@ -1000,7 +990,7 @@ protected static boolean isEEEx() { * @return {@code BaseOpcode} instance of the requested opcode. Returns a default opcode instance for unknown or * unsupported opcodes. */ - protected static BaseOpcode getOpcode(int id) { + public static BaseOpcode getOpcode(int id) { final BaseOpcode opcode = getOpcodeMap().get(id); if (opcode != null) { return opcode; @@ -1009,6 +999,16 @@ protected static BaseOpcode getOpcode(int id) { } } + /** Returns whether the current game is enhanced by TobEx. */ + protected static boolean isTobEx() { + return Profile.getProperty(Profile.Key.IS_GAME_TOBEX); + } + + /** Returns whether the current game is enhanced by EEEx. */ + protected static boolean isEEEx() { + return Profile.getProperty(Profile.Key.IS_GAME_EEEX); + } + /** Used internally to return a valid opcode list for the current game. */ private static synchronized TreeMap getOpcodeMap() { if (opcodeList == null) { diff --git a/src/org/infinity/resource/effects/Opcode101.java b/src/org/infinity/resource/effects/Opcode101.java index 309f44bbe..eff538639 100644 --- a/src/org/infinity/resource/effects/Opcode101.java +++ b/src/org/infinity/resource/effects/Opcode101.java @@ -7,9 +7,9 @@ import java.nio.ByteBuffer; import java.util.List; -import org.infinity.datatype.Bitmap; import org.infinity.datatype.Datatype; import org.infinity.datatype.DecNumber; +import org.infinity.datatype.EffectBitmap; import org.infinity.resource.AbstractStruct; import org.infinity.resource.StructEntry; @@ -30,7 +30,7 @@ public Opcode101() { protected String makeEffectParamsGeneric(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new DecNumber(buffer, offset, 4, AbstractStruct.COMMON_UNUSED)); - list.add(new Bitmap(buffer, offset + 4, 4, "Effect", getEffectNames())); + list.add(new EffectBitmap(buffer, offset + 4, 4)); return null; } } diff --git a/src/org/infinity/resource/effects/Opcode198.java b/src/org/infinity/resource/effects/Opcode198.java index 64270b478..939ece295 100644 --- a/src/org/infinity/resource/effects/Opcode198.java +++ b/src/org/infinity/resource/effects/Opcode198.java @@ -7,9 +7,9 @@ import java.nio.ByteBuffer; import java.util.List; -import org.infinity.datatype.Bitmap; import org.infinity.datatype.Datatype; import org.infinity.datatype.DecNumber; +import org.infinity.datatype.EffectBitmap; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Profile; import org.infinity.resource.StructEntry; @@ -18,8 +18,6 @@ * Implemention of opcode 198. */ public class Opcode198 extends BaseOpcode { - private static final String EFFECT_FX = "Effect"; - /** Returns the opcode name for the current game variant. */ private static String getOpcodeName() { switch (Profile.getEngine()) { @@ -41,7 +39,7 @@ public Opcode198() { protected String makeEffectParamsGeneric(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new DecNumber(buffer, offset, 4, AbstractStruct.COMMON_UNUSED)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_FX, getEffectNames())); + list.add(new EffectBitmap(buffer, offset + 4, 4)); return null; } diff --git a/src/org/infinity/resource/effects/Opcode261.java b/src/org/infinity/resource/effects/Opcode261.java index c16312936..a6412045c 100644 --- a/src/org/infinity/resource/effects/Opcode261.java +++ b/src/org/infinity/resource/effects/Opcode261.java @@ -10,6 +10,7 @@ import org.infinity.datatype.Bitmap; import org.infinity.datatype.Datatype; import org.infinity.datatype.DecNumber; +import org.infinity.datatype.EffectBitmap; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Profile; import org.infinity.resource.StructEntry; @@ -20,7 +21,6 @@ public class Opcode261 extends BaseOpcode { private static final String EFFECT_SPELL_LEVEL = "Spell level"; private static final String EFFECT_SPELL_CLASS = "Spell class"; - private static final String EFFECT_FX = "Effect"; private static final String RES_TYPE_IWD2 = "SPL"; @@ -63,7 +63,7 @@ protected String makeEffectParamsBG1(Datatype parent, ByteBuffer buffer, int off protected String makeEffectParamsIWD(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new DecNumber(buffer, offset, 4, AbstractStruct.COMMON_UNUSED)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_FX, getEffectNames())); + list.add(new EffectBitmap(buffer, offset + 4, 4)); return null; } @@ -71,7 +71,7 @@ protected String makeEffectParamsIWD(Datatype parent, ByteBuffer buffer, int off protected String makeEffectParamsIWD2(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new DecNumber(buffer, offset, 4, AbstractStruct.COMMON_UNUSED)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_FX, getEffectNames())); + list.add(new EffectBitmap(buffer, offset + 4, 4)); return RES_TYPE_IWD2; } diff --git a/src/org/infinity/resource/effects/Opcode276.java b/src/org/infinity/resource/effects/Opcode276.java index 4c5b69baf..c300ddde6 100644 --- a/src/org/infinity/resource/effects/Opcode276.java +++ b/src/org/infinity/resource/effects/Opcode276.java @@ -10,6 +10,7 @@ import org.infinity.datatype.Bitmap; import org.infinity.datatype.Datatype; import org.infinity.datatype.DecNumber; +import org.infinity.datatype.EffectBitmap; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Profile; import org.infinity.resource.StructEntry; @@ -18,8 +19,6 @@ * Implemention of opcode 276. */ public class Opcode276 extends BaseOpcode { - private static final String EFFECT_FX = "Effect"; - /** Returns the opcode name for the current game variant. */ private static String getOpcodeName() { switch (Profile.getEngine()) { @@ -56,7 +55,7 @@ protected String makeEffectParamsBG1(Datatype parent, ByteBuffer buffer, int off protected String makeEffectParamsIWD(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new DecNumber(buffer, offset, 4, AbstractStruct.COMMON_UNUSED)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_FX, getEffectNames())); + list.add(new EffectBitmap(buffer, offset + 4, 4)); return null; } diff --git a/src/org/infinity/resource/effects/Opcode337.java b/src/org/infinity/resource/effects/Opcode337.java index c45f2f69c..78a5016a1 100644 --- a/src/org/infinity/resource/effects/Opcode337.java +++ b/src/org/infinity/resource/effects/Opcode337.java @@ -7,9 +7,9 @@ import java.nio.ByteBuffer; import java.util.List; -import org.infinity.datatype.Bitmap; import org.infinity.datatype.Datatype; import org.infinity.datatype.DecNumber; +import org.infinity.datatype.EffectBitmap; import org.infinity.resource.Profile; import org.infinity.resource.StructEntry; @@ -18,7 +18,6 @@ */ public class Opcode337 extends BaseOpcode { private static final String EFFECT_MATCH_P2_VALUE = "Match 'Parameter 2' value"; - private static final String EFFECT_FX = "Effect"; /** Returns the opcode name for the current game variant. */ private static String getOpcodeName() { @@ -38,7 +37,7 @@ public Opcode337() { protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new DecNumber(buffer, offset, 4, EFFECT_MATCH_P2_VALUE)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_FX, getEffectNames())); + list.add(new EffectBitmap(buffer, offset + 4, 4)); return null; } } diff --git a/src/org/infinity/resource/graphics/BamResource.java b/src/org/infinity/resource/graphics/BamResource.java index 0e7238579..e355f669c 100644 --- a/src/org/infinity/resource/graphics/BamResource.java +++ b/src/org/infinity/resource/graphics/BamResource.java @@ -247,6 +247,10 @@ public void actionPerformed(ActionEvent event) { if (ResourceFactory.saveResource(this, panelMain.getTopLevelAncestor())) { setRawModified(false); } + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == event.getSource()) { + if (ResourceFactory.saveResourceAs(this, panelMain.getTopLevelAncestor())) { + setRawModified(false); + } } else if (event.getSource() == timer) { if (curCycle >= 0) { curFrame = (curFrame + 1) % bamControl.cycleFrameCount(); @@ -565,6 +569,8 @@ public JComponent makeViewer(ViewableContainer container) { buttonPanel.addControl(bpmExport, ButtonPanel.Control.EXPORT_MENU); ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(false); + ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS)).addActionListener(this); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(false); buttonPanel.addControl(bProperties, PROPERTIES); buttonPanel.addControl(bEdit, BAM_EDIT); buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); @@ -1302,6 +1308,7 @@ private void setRawModified(boolean modified) { hexViewer.clearModified(); } buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(modified); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(modified); } } } diff --git a/src/org/infinity/resource/graphics/BmpDecoder.java b/src/org/infinity/resource/graphics/BmpDecoder.java new file mode 100644 index 000000000..ae80f98bb --- /dev/null +++ b/src/org/infinity/resource/graphics/BmpDecoder.java @@ -0,0 +1,308 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics; + +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +import javax.imageio.ImageIO; + +import org.infinity.resource.key.ResourceEntry; +import org.infinity.util.io.ByteBufferInputStream; +import org.infinity.util.io.FileManager; +import org.infinity.util.io.StreamUtils; + +/** + * Decodes a BMP file. + */ +public class BmpDecoder { + private BufferedImage image; + private Palette palette; + private Info info; + + /** + * Returns an initialized {@code BmpDecoder} object with the specified {@code ResourceEntry} instance. + * + * @param entry The {@link ResourceEntry} instance of the BMP resource. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(ResourceEntry entry) throws Exception { + return new BmpDecoder(Objects.requireNonNull(entry).getResourceBuffer()); + } + + /** + * Returns an initialized {@code BmpDecoder} object with the specified file. + * + * @param file Filename of the BMP resource as {@code String}. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(String file) throws Exception { + return loadBmp(FileManager.resolve(file)); + } + + /** + * Returns an initialized {@code BmpDecoder} object with the specified {@code Path} instance. + * + * @param file Filename of the BMP resource as {@link Path} instance. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(Path file) throws Exception { + try (InputStream is = StreamUtils.getInputStream(Objects.requireNonNull(file))) { + return loadBmp(is); + } + } + + /** + * Returns an initialized {@code BmpDecoder} object with data from the specified {@code InputStream} instance. + * + * @param input The {@link InputStream} instance containing BMP data. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(InputStream input) throws Exception { + Objects.requireNonNull(input); + final int bufSize = 1024; + final ArrayList bufList = new ArrayList<>(); + while (true) { + final byte[] buf = new byte[bufSize]; + int len = input.read(buf); + if (len == bufSize) { + bufList.add(buf); + } else if (len > 0) { + bufList.add(Arrays.copyOf(buf, len)); + } else { + break; + } + } + + int bufferSize = bufList.stream().mapToInt(b -> b.length).sum(); + final ByteBuffer bb = StreamUtils.getByteBuffer(bufferSize); + bufList.forEach(b -> bb.put(b)); + bb.rewind(); + + return new BmpDecoder(bb); + } + + private BmpDecoder(ByteBuffer buffer) throws Exception { + init(buffer); + } + + /** Returns information about the BMP image. */ + public Info getInfo() { + return info; + } + + /** Returns the decoded BMP resource as {@link Image} object. */ + public BufferedImage getImage() { + return image; + } + + /** + * Returns the palette for indexed BMP resources. + * + * @return {@link Palette} object for indexed BMP resources, {@code null} otherwise. + */ + public Palette getPalette() { + return palette; + } + + private void init(ByteBuffer buffer) throws Exception { + Objects.requireNonNull(buffer); + + // Checking signature + boolean isBMP = false; + if ("BM".equals(StreamUtils.readString(buffer, 0, 2))) { + isBMP = true; + } + + image = null; + palette = null; + if (isBMP) { + int rasteroff = buffer.getInt(10); + + int width = buffer.getInt(18); + int height = buffer.getInt(22); + int bitcount = buffer.getShort(28); + int compression = buffer.getInt(30); + if ((compression == 0 || compression == 3) && bitcount <= 32) { + int colsUsed = buffer.getInt(46); // Colorsused + + if (bitcount <= 8) { + if (colsUsed == 0) { + colsUsed = 1 << bitcount; + } + int palSize = 4 * colsUsed; + palette = new Palette(buffer, rasteroff - palSize, palSize); + } + + int bytesprline = bitcount * width / 8; + int padded = 4 - bytesprline % 4; + if (padded == 4) { + padded = 0; + } + + image = ColorConvert.createCompatibleImage(width, height, bitcount >= 32); + int offset = rasteroff; + for (int y = height - 1; y >= 0; y--) { + setPixels(buffer, offset, bitcount, bytesprline, y, palette); + offset += bytesprline + padded; + } + + info = new Info(image, compression, bitcount); + } + } + + if (image == null) { + buffer.rewind(); + try (ByteBufferInputStream bbis = new ByteBufferInputStream(buffer)) { + image = ImageIO.read(bbis); + // extracting palette + if (image.getColorModel() instanceof IndexColorModel) { + final IndexColorModel icm = (IndexColorModel) image.getColorModel(); + int numColors = icm.getMapSize(); + final int[] colors = new int[numColors]; + icm.getRGBs(colors); + final ByteBuffer pb = StreamUtils.getByteBuffer(colors.length * 4); + for (int i = 0; i < colors.length; i++) { + pb.putInt(colors[i]); + } + pb.rewind(); + palette = new Palette(pb, 0, pb.capacity()); + } + info = new Info(image); + } catch (Exception e) { + image = null; + palette = null; + throw new Exception("Unsupported graphics format"); + } + } + } + + private void setPixels(ByteBuffer buffer, int offset, int bitcount, int width, int y, Palette palette) { + if (bitcount == 4) { + int pix = 0; + for (int x = 0; x < width; x++) { + int color = buffer.get(offset + x) & 0xff; + int color1 = (color >> 4) & 0x0f; + image.setRGB(pix++, y, palette.getColor(color1)); + int color2 = color & 0x0f; + image.setRGB(pix++, y, palette.getColor(color2)); + } + } else if (bitcount == 8) { + for (int x = 0; x < width; x++) { + image.setRGB(x, y, palette.getColor(buffer.get(offset + x) & 0xff)); + } + } else if (bitcount == 24) { + for (int x = 0; x < width / 3; x++) { + int rgb = (buffer.get(offset + 3 * x + 2) & 0xff) << 16; + rgb |= (buffer.get(offset + 3 * x + 1) & 0xff) << 8; + rgb |= buffer.get(offset + 3 * x) & 0xff; + image.setRGB(x, y, rgb); + } + } else if (bitcount == 32) { + for (int x = 0; x < width / 4; x++) { + int rgb = buffer.getInt(offset + 4 * x); + image.setRGB(x, y, rgb); + } + } + } + + // -------------------------- INNER CLASSES -------------------------- + + /** + * Provides basic information about the BMP resource. + */ + public static class Info { + /** Available bitmap compression types. */ + public enum Compression { + /** Compression type could not be determined. */ + UNKNOWN(-1, "Unknown"), + /** Uncompressed RGB pixel data. */ + RGB(0, "No compression"), + /** RLE-encoded 8-bit paletted pixel data. */ + RLE8(1, "RLE encoded (8-bit)"), + /** RLE-encoded 4-bit paletted pixel data. */ + RLE4(2, "RLE encoded (4-bit)"), + /** Color components are defined by component masks. */ + BITFIELD(3, "Bitfield encoded"), + ; + + private final int code; + private final String label; + + private Compression(int code, String label) { + this.code = code; + this.label = label; + } + + /** Returns the numeric BMP compression code. Returns -1 for undetermined compression. */ + public int getCode() { + return code; + } + + /** Returns a descriptive label for the compression. */ + public String getLabel() { + return label; + } + + @Override + public String toString() { + return String.format("%s (%d)", getLabel(), getCode()); + } + } + + private Compression compression; + private int width; + private int height; + private int bpp; + + private Info(BufferedImage image) { + this(image, -1, 0); + } + + private Info(BufferedImage image, int compression, int bpp) { + Objects.requireNonNull(image); + + this.compression = Arrays + .stream(Compression.values()) + .filter(c -> c.getCode() == compression) + .findFirst() + .orElse(Compression.UNKNOWN); + this.width = image.getWidth(); + this.height = image.getHeight(); + this.bpp = (bpp > 0) ? bpp : image.getColorModel().getPixelSize(); + } + + /** Returns the compression type of the BMP resource. */ + public Compression getCompression() { + return compression; + } + + /** Returns the image width, in pixels. */ + public int getWidth() { + return width; + } + + /** Returns the image height, in pixels. */ + public int getHeight() { + return height; + } + + /** Returns the number of bits per pixel. */ + public int getBitsPerPixel() { + return bpp; + } + } +} diff --git a/src/org/infinity/resource/graphics/GraphicsResource.java b/src/org/infinity/resource/graphics/GraphicsResource.java index 98dcd8815..ccd3b9f54 100644 --- a/src/org/infinity/resource/graphics/GraphicsResource.java +++ b/src/org/infinity/resource/graphics/GraphicsResource.java @@ -9,37 +9,38 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; -import java.io.InputStream; -import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.function.Function; -import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import org.infinity.gui.ButtonPanel; import org.infinity.gui.RenderCanvas; +import org.infinity.icon.Icons; import org.infinity.resource.Referenceable; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.ViewableContainer; import org.infinity.resource.key.ResourceEntry; import org.infinity.search.ReferenceSearcher; -import org.infinity.util.io.StreamUtils; public final class GraphicsResource implements Resource, Referenceable, ActionListener { + private static final ButtonPanel.Control PROPERTIES = ButtonPanel.Control.CUSTOM_1; + private final ResourceEntry entry; + private final BmpDecoder decoder; private final ButtonPanel buttonPanel = new ButtonPanel(); - private BufferedImage image; private JPanel panel; - private Palette palette; public GraphicsResource(ResourceEntry entry) throws Exception { this.entry = entry; - init(); + this.decoder = BmpDecoder.loadBmp(entry); } // --------------------- Begin Interface ActionListener --------------------- @@ -50,6 +51,8 @@ public void actionPerformed(ActionEvent event) { searchReferences(panel.getTopLevelAncestor()); } else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); + } else if (buttonPanel.getControlByType(PROPERTIES) == event.getSource()) { + showProperties(); } } @@ -82,7 +85,7 @@ public void searchReferences(Component parent) { @Override public JComponent makeViewer(ViewableContainer container) { - RenderCanvas rcCanvas = new RenderCanvas(image); + RenderCanvas rcCanvas = new RenderCanvas(decoder.getImage()); JScrollPane scroll = new JScrollPane(rcCanvas); scroll.getVerticalScrollBar().setUnitIncrement(16); scroll.getHorizontalScrollBar().setUnitIncrement(16); @@ -90,6 +93,11 @@ public JComponent makeViewer(ViewableContainer container) { ((JButton) buttonPanel.addControl(ButtonPanel.Control.FIND_REFERENCES)).addActionListener(this); ((JButton) buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this); + JButton bProperties = new JButton("Properties...", Icons.ICON_EDIT_16.getIcon()); + bProperties.setMnemonic('p'); + bProperties.addActionListener(this); + buttonPanel.addControl(bProperties, PROPERTIES); + panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(scroll, BorderLayout.CENTER); @@ -101,90 +109,28 @@ public JComponent makeViewer(ViewableContainer container) { // --------------------- End Interface Viewable --------------------- public BufferedImage getImage() { - return image; + return decoder.getImage(); } public Palette getPalette() { - return palette; + return decoder.getPalette(); } - private void init() throws Exception { - ByteBuffer buffer = entry.getResourceBuffer(); - // Checking signature - boolean isBMP = false; - if ("BM".equals(StreamUtils.readString(buffer, 0, 2))) { - isBMP = true; - } - - image = null; - if (isBMP) { - int rasteroff = buffer.getInt(10); - - int width = buffer.getInt(18); - int height = buffer.getInt(22); - int bitcount = buffer.getShort(28); - int compression = buffer.getInt(30); - if ((compression == 0 || compression == 3) && bitcount <= 32) { - int colsUsed = buffer.getInt(46); // Colorsused - - if (bitcount <= 8) { - if (colsUsed == 0) { - colsUsed = 1 << bitcount; - } - int palSize = 4 * colsUsed; - palette = new Palette(buffer, rasteroff - palSize, palSize); - } - - int bytesprline = bitcount * width / 8; - int padded = 4 - bytesprline % 4; - if (padded == 4) { - padded = 0; - } - - image = ColorConvert.createCompatibleImage(width, height, bitcount >= 32); - int offset = rasteroff; - for (int y = height - 1; y >= 0; y--) { - setPixels(buffer, offset, bitcount, bytesprline, y, palette); - offset += bytesprline + padded; - } - } - } - if (image == null) { - try (InputStream is = entry.getResourceDataAsStream()) { - image = ImageIO.read(is); - } catch (Exception e) { - image = null; - throw new Exception("Unsupported graphics format"); - } - } + public BmpDecoder.Info getInfo() { + return decoder.getInfo(); } - private void setPixels(ByteBuffer buffer, int offset, int bitcount, int width, int y, Palette palette) { - if (bitcount == 4) { - int pix = 0; - for (int x = 0; x < width; x++) { - int color = buffer.get(offset + x) & 0xff; - int color1 = (color >> 4) & 0x0f; - image.setRGB(pix++, y, palette.getColor(color1)); - int color2 = color & 0x0f; - image.setRGB(pix++, y, palette.getColor(color2)); - } - } else if (bitcount == 8) { - for (int x = 0; x < width; x++) { - image.setRGB(x, y, palette.getColor(buffer.get(offset + x) & 0xff)); - } - } else if (bitcount == 24) { - for (int x = 0; x < width / 3; x++) { - int rgb = (buffer.get(offset + 3 * x + 2) & 0xff) << 16; - rgb |= (buffer.get(offset + 3 * x + 1) & 0xff) << 8; - rgb |= buffer.get(offset + 3 * x) & 0xff; - image.setRGB(x, y, rgb); - } - } else if (bitcount == 32) { - for (int x = 0; x < width / 4; x++) { - int rgb = buffer.getInt(offset + 4 * x); - image.setRGB(x, y, rgb); - } - } + private void showProperties() { + // Width, Height, BitsPerPixel, Compression + final Function space = (i) -> new String(new char[i]).replace("\0", " "); + final String br = "
"; + final String resName = entry.getResourceName().toUpperCase(Locale.ENGLISH); + final StringBuilder sb = new StringBuilder("
"); + sb.append("Width:").append(space.apply(7)).append(getInfo().getWidth()).append(br); + sb.append("Height:").append(space.apply(6)).append(getInfo().getHeight()).append(br); + sb.append("Bits/Pixel:").append(space.apply(2)).append(getInfo().getBitsPerPixel()).append(br); + sb.append("
"); + + JOptionPane.showMessageDialog(panel, sb.toString(), "Properties of " + resName, JOptionPane.INFORMATION_MESSAGE); } } diff --git a/src/org/infinity/resource/graphics/PltResource.java b/src/org/infinity/resource/graphics/PltResource.java index e9b743497..f4e5f6f0a 100644 --- a/src/org/infinity/resource/graphics/PltResource.java +++ b/src/org/infinity/resource/graphics/PltResource.java @@ -215,6 +215,8 @@ public JComponent makeViewer(ViewableContainer container) { ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(false); + ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS)).addActionListener(this); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(false); buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); tabbedPane = new JTabbedPane(SwingConstants.TOP); @@ -292,6 +294,10 @@ public void actionPerformed(ActionEvent e) { if (ResourceFactory.saveResource(this, panelMain.getTopLevelAncestor())) { setRawModified(false); } + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == e.getSource()) { + if (ResourceFactory.saveResourceAs(this, panelMain.getTopLevelAncestor())) { + setRawModified(false); + } } } @@ -439,6 +445,7 @@ private void setRawModified(boolean modified) { hexViewer.clearModified(); } buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(modified); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(modified); } } diff --git a/src/org/infinity/resource/key/FileResourceEntry.java b/src/org/infinity/resource/key/FileResourceEntry.java index d4459291d..fec931674 100644 --- a/src/org/infinity/resource/key/FileResourceEntry.java +++ b/src/org/infinity/resource/key/FileResourceEntry.java @@ -156,6 +156,15 @@ public ResourceTreeFolder getTreeFolder() { retVal = ResourceFactory.getResourceTreeModel().getFolder(getTreeFolderName()); } + // check "Special" folder + if (retVal == null) { + final ResourceTreeFolder specialFolder = + ResourceFactory.getResourceTreeModel().getFolder(ResourceFactory.SPECIAL_CATEGORY); + if (specialFolder.getResourceEntries().contains(this)) { + retVal = specialFolder; + } + } + return retVal; } diff --git a/src/org/infinity/resource/mus/MusResource.java b/src/org/infinity/resource/mus/MusResource.java index 4dff7191d..4707a64c4 100644 --- a/src/org/infinity/resource/mus/MusResource.java +++ b/src/org/infinity/resource/mus/MusResource.java @@ -187,6 +187,11 @@ public void actionPerformed(ActionEvent event) { setDocumentModified(false); } viewer.loadMusResource(this); + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == event.getSource()) { + if (ResourceFactory.saveResourceAs(this, panel.getTopLevelAncestor())) { + setDocumentModified(false); + } + viewer.loadMusResource(this); } else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); } @@ -339,6 +344,9 @@ private JComponent getEditor(CaretListener caretListener) { JButton bSave = (JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE); bSave.addActionListener(this); bSave.setEnabled(getDocumentModified()); + JButton bSaveAs = (JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS); + bSaveAs.addActionListener(this); + bSaveAs.setEnabled(getDocumentModified()); JPanel lowerpanel = new JPanel(); lowerpanel.setLayout(new FlowLayout(FlowLayout.CENTER)); @@ -360,6 +368,7 @@ private void setDocumentModified(boolean b) { if (b != resourceChanged) { resourceChanged = b; buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(resourceChanged); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(resourceChanged); } } } diff --git a/src/org/infinity/resource/other/FntResource.java b/src/org/infinity/resource/other/FntResource.java index 7e6171e63..27ffca1e0 100644 --- a/src/org/infinity/resource/other/FntResource.java +++ b/src/org/infinity/resource/other/FntResource.java @@ -89,5 +89,9 @@ protected void viewerInitialized(StructViewer viewer) { if (bSave != null) { bSave.setEnabled(false); } + JButton bSaveAs = (JButton) viewer.getButtonPanel().getControlByType(ButtonPanel.Control.SAVE_AS); + if (bSaveAs != null) { + bSaveAs.setEnabled(false); + } } } diff --git a/src/org/infinity/resource/other/UnknownResource.java b/src/org/infinity/resource/other/UnknownResource.java index cf274148b..cfd7a466a 100644 --- a/src/org/infinity/resource/other/UnknownResource.java +++ b/src/org/infinity/resource/other/UnknownResource.java @@ -120,6 +120,11 @@ public void actionPerformed(ActionEvent event) { setTextModified(false); setRawModified(false); } + } else if (event.getSource() == buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS)) { + if (ResourceFactory.saveResourceAs(this, panelMain.getTopLevelAncestor())) { + setTextModified(false); + setRawModified(false); + } } } @@ -180,6 +185,8 @@ public JComponent makeViewer(ViewableContainer container) { ((JButton) buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this); ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(false); + ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS)).addActionListener(this); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(false); GridBagConstraints gbc = new GridBagConstraints(); @@ -348,6 +355,7 @@ private void synchronizeData(int tabIndex) { private void setSaveButtonEnabled(boolean enable) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(enable); + buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS).setEnabled(enable); } private void updateStatusBar() { diff --git a/src/org/infinity/resource/pro/ProAreaType.java b/src/org/infinity/resource/pro/ProAreaType.java index 7b98de038..0d90dacc7 100644 --- a/src/org/infinity/resource/pro/ProAreaType.java +++ b/src/org/infinity/resource/pro/ProAreaType.java @@ -63,7 +63,7 @@ public final class ProAreaType extends AbstractStruct implements AddRemovable, U public static final String[] AREA_FLAGS_EX_ARRAY = { "No flags set", "Paletted ring", "Random speed", "Start scattered", "Paletted center", "Repeat scattering", "Paletted animation", null, null, null, - "Oriented fireball puffs", "Use hit dice lookup", null, null, "Blend are/ring anim", "Glow area/ring anim", + "Oriented fireball puffs", "Use hit dice lookup", null, null, "Blend area/ring anim", "Glow area/ring anim", "Hit point limit", }; static { diff --git a/src/org/infinity/resource/sav/IOHandler.java b/src/org/infinity/resource/sav/IOHandler.java index 7b777bc93..82f37fece 100644 --- a/src/org/infinity/resource/sav/IOHandler.java +++ b/src/org/infinity/resource/sav/IOHandler.java @@ -31,7 +31,7 @@ public final class IOHandler implements Writeable { private Path tempFolder; - public IOHandler(ResourceEntry entry) throws Exception { + public IOHandler(ResourceEntry entry, boolean sortByName) throws Exception { this.entry = entry; ByteBuffer buffer = entry.getResourceBuffer(true); // ignoreOverride - no real effect header = new TextString(buffer, 0, 8, null); @@ -45,7 +45,9 @@ public IOHandler(ResourceEntry entry) throws Exception { fileEntries.add(fileEntry); offset = fileEntry.getEndOffset(); } - Collections.sort(fileEntries); + if (sortByName) { + Collections.sort(fileEntries); + } } // --------------------- Begin Interface Writeable --------------------- @@ -119,7 +121,7 @@ public List decompress() throws Exception { return entries; } - public List getFileEntries() { + public List getFileEntries() { return fileEntries; } diff --git a/src/org/infinity/resource/sav/SavResource.java b/src/org/infinity/resource/sav/SavResource.java index 6b26afdfb..56e136bab 100644 --- a/src/org/infinity/resource/sav/SavResource.java +++ b/src/org/infinity/resource/sav/SavResource.java @@ -81,9 +81,34 @@ public final class SavResource implements Resource, Closeable, Writeable, Action private JMenuItem miAddExternal; private JMenuItem miAddInternal; + /** + * Decompresses and returns a specific resource from the given SAV resource. + * + * @param savEntry The SAV resource containing the given resource. + * @param resourceName Full name (name dot extension) of the resource to load. + * @return {@link Resource} instance of the given resource. Returns {@code null} if the resource is not available. + * @throws Exception + */ + public static Resource loadResource(ResourceEntry savEntry, String resourceName) throws Exception { + Resource retVal = null; + + final IOHandler handler = new IOHandler(savEntry, false); + final SavResourceEntry resEntry = handler + .getFileEntries() + .stream() + .filter(e -> e.getResourceName().equalsIgnoreCase(resourceName)) + .findFirst() + .orElse(null); + if (resEntry != null) { + retVal = ResourceFactory.getResource(resEntry); + } + + return retVal; + } + public SavResource(ResourceEntry entry) throws Exception { this.entry = entry; - handler = new IOHandler(entry); + handler = new IOHandler(entry, true); } // --------------------- Begin Interface ActionListener --------------------- diff --git a/src/org/infinity/resource/sto/ItemSale11.java b/src/org/infinity/resource/sto/ItemSale11.java index 205f86893..f42e00beb 100644 --- a/src/org/infinity/resource/sto/ItemSale11.java +++ b/src/org/infinity/resource/sto/ItemSale11.java @@ -12,6 +12,8 @@ import org.infinity.datatype.ResourceRef; import org.infinity.datatype.StringRef; import org.infinity.datatype.Unknown; +import org.infinity.gui.InfinityTextArea; +import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.resource.AbstractStruct; import org.infinity.resource.AddRemovable; import org.infinity.util.io.StreamUtils; @@ -56,7 +58,9 @@ public int read(ByteBuffer buffer, int offset) throws Exception { addField(new Flag(buffer, offset + 16, 4, STO_SALE_FLAGS, ITEM_FLAGS_ARRAY)); addField(new DecNumber(buffer, offset + 20, 4, STO_SALE_NUM_IN_STOCK)); addField(new Bitmap(buffer, offset + 24, 4, STO_SALE_INFINITE_SUPPLY, OPTION_NOYES)); - addField(new StringRef(buffer, offset + 28, STO_SALE_TRIGGER)); + final InfinityTextArea.Language languageOverride = + BrowserMenuBar.getInstance().getOptions().getBcsSyntaxHighlightingEnabled() ? InfinityTextArea.Language.BCS : null; + addField(new StringRef(buffer, offset + 28, STO_SALE_TRIGGER, languageOverride)); addField(new Unknown(buffer, offset + 32, 56)); return offset + 88; } diff --git a/src/org/infinity/resource/text/PlainTextResource.java b/src/org/infinity/resource/text/PlainTextResource.java index 1d417d995..5b0f99c77 100644 --- a/src/org/infinity/resource/text/PlainTextResource.java +++ b/src/org/infinity/resource/text/PlainTextResource.java @@ -340,6 +340,10 @@ public void actionPerformed(ActionEvent event) { if (ResourceFactory.saveResource(this, panel.getTopLevelAncestor())) { resourceChanged = false; } + } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE_AS) == event.getSource()) { + if (ResourceFactory.saveResourceAs(this, panel.getTopLevelAncestor())) { + resourceChanged = false; + } } else if (buttonPanel.getControlByType(ButtonPanel.Control.FIND_REFERENCES) == event.getSource()) { searchReferences(panel.getTopLevelAncestor()); } else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) { @@ -521,6 +525,7 @@ public JComponent makeViewer(ViewableContainer container) { } ((JButton) buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this); ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); + ((JButton) buttonPanel.addControl(ButtonPanel.Control.SAVE_AS)).addActionListener(this); panel = new JPanel(); panel.setLayout(new BorderLayout()); diff --git a/src/org/infinity/resource/to/TohResource.java b/src/org/infinity/resource/to/TohResource.java index 9313ae36e..09ea40170 100644 --- a/src/org/infinity/resource/to/TohResource.java +++ b/src/org/infinity/resource/to/TohResource.java @@ -11,6 +11,7 @@ import javax.swing.JButton; import org.infinity.datatype.DecNumber; +import org.infinity.datatype.IsNumeric; import org.infinity.datatype.SectionCount; import org.infinity.datatype.SectionOffset; import org.infinity.datatype.TextString; @@ -112,10 +113,101 @@ public int read(ByteBuffer buffer, int offset) throws Exception { @Override protected void viewerInitialized(StructViewer viewer) { - // disabling 'Save' button + // disabling 'Save' buttons JButton bSave = (JButton) viewer.getButtonPanel().getControlByType(ButtonPanel.Control.SAVE); if (bSave != null) { bSave.setEnabled(false); } + JButton bSaveAs = (JButton) viewer.getButtonPanel().getControlByType(ButtonPanel.Control.SAVE_AS); + if (bSaveAs != null) { + bSave.setEnabled(false); + } + } + + /** + * Returns the string for {@code strref} from the given string override resource entries. + * + * @param tohEntry {@link ResourceEntry} instance referencing a TOH resource. Required by both EE and non-EE games. + * @param totEntry {@link ResourceEntry} instance referencing a TOT resource. Required only by non-EE games. + * @param strref The string reference to look up. + * @return String referenced by {@code strref} if found, {@code null} otherwise. + */ + public static String getOverrideString(ResourceEntry tohEntry, ResourceEntry totEntry, int strref) { + TohResource toh = null; + TotResource tot = null; + try { + toh = (tohEntry != null) ? new TohResource(tohEntry) : null; + } catch (Exception e) { + e.printStackTrace(); + } + if (!Profile.isEnhancedEdition()) { + try { + tot = (totEntry != null) ? new TotResource(totEntry, toh) : null; + } catch (Exception e) { + e.printStackTrace(); + } + } + return getOverrideString(toh, tot, strref); + } + + /** + * Returns the string for {@code strref} from the given string override resources. + * + * @param toh {@link TohResource} instance. Required by both EE and non-EE games. + * @param tot {@link TotResource} instance. Required only by non-EE games. + * @param strref The string reference to look up. + * @return String referenced by {@code strref} if found, {@code null} otherwise. + */ + public static String getOverrideString(TohResource toh, TotResource tot, int strref) { + String retVal = null; + + if (strref < 0 || toh == null || (!Profile.isEnhancedEdition() && tot == null)) { + return retVal; + } + + if (Profile.isEnhancedEdition()) { + // Only TOH resource is needed + IsNumeric so = (IsNumeric) toh.getAttribute(TohResource.TOH_OFFSET_ENTRIES); + IsNumeric sc = (IsNumeric) toh.getAttribute(TohResource.TOH_NUM_ENTRIES); + if (so != null && sc != null && sc.getValue() > 0) { + for (int i = 0, count = sc.getValue(), curOfs = so.getValue(); i < count; i++) { + StrRefEntry2 strrefEntry = (StrRefEntry2) toh.getAttribute(curOfs, false); + if (strrefEntry != null) { + int v = ((IsNumeric) strrefEntry.getAttribute(StrRefEntry2.TOH_STRREF_OVERRIDDEN)).getValue(); + if (v == strref) { + int sofs = ((IsNumeric) strrefEntry.getAttribute(StrRefEntry2.TOH_STRREF_OFFSET_STRING)).getValue(); + StringEntry2 se = (StringEntry2) toh.getAttribute(so.getValue() + sofs, false); + if (se != null) { + retVal = se.getAttribute(StringEntry2.TOH_STRING_TEXT).toString(); + } + break; + } + curOfs += strrefEntry.getSize(); + } + } + } + } else { + // Utilizing both TOT and TOH + IsNumeric sc = (IsNumeric) toh.getAttribute(TohResource.TOH_NUM_ENTRIES); + if (sc != null && sc.getValue() > 0) { + for (int i = 0, count = sc.getValue(), curOfs = 0x14; i < count; i++) { + StrRefEntry strrefEntry = (StrRefEntry) toh.getAttribute(curOfs, false); + if (strrefEntry != null) { + int v = ((IsNumeric) strrefEntry.getAttribute(StrRefEntry.TOH_STRREF_OVERRIDDEN)).getValue(); + if (v == strref) { + int sofs = ((IsNumeric) strrefEntry.getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING)).getValue(); + StringEntry se = (StringEntry) tot.getAttribute(sofs, false); + if (se != null) { + retVal = se.getAttribute(StringEntry.TOT_STRING_TEXT).toString(); + } + break; + } + curOfs += strrefEntry.getSize(); + } + } + } + } + + return retVal; } } diff --git a/src/org/infinity/resource/to/TotResource.java b/src/org/infinity/resource/to/TotResource.java index 384b0d7f5..5d6703e36 100644 --- a/src/org/infinity/resource/to/TotResource.java +++ b/src/org/infinity/resource/to/TotResource.java @@ -39,7 +39,11 @@ public final class TotResource extends AbstractStruct implements Resource { public static final String TOT_EMPTY = "(empty)"; public TotResource(ResourceEntry entry) throws Exception { - super(entry); + this(entry, null); + } + + public TotResource(ResourceEntry entry, TohResource toh) throws Exception { + super(entry, toh); } @Override @@ -114,9 +118,9 @@ public int read(ByteBuffer buffer, int offset) throws Exception { * @return {@code TohResource} instance if loaded successfully, {@code null} otherwise. */ private TohResource loadAssociatedToh(ResourceEntry totResource) { - TohResource toh = null; + TohResource toh = (getExtraData() instanceof TohResource) ? (TohResource) getExtraData() : null; - if (totResource != null) { + if (toh == null && totResource != null) { final Path totPath = totResource.getActualPath(); if (totPath != null) { String fileName = totPath.getName(totPath.getNameCount() - 1).toString(); diff --git a/src/org/infinity/search/AbstractSearcher.java b/src/org/infinity/search/AbstractSearcher.java index 588261ab7..7096505a5 100644 --- a/src/org/infinity/search/AbstractSearcher.java +++ b/src/org/infinity/search/AbstractSearcher.java @@ -94,7 +94,7 @@ protected boolean runSearch(String operation, List entries) { boolean isCancelled = false; Debugging.timerReset(); int i = 0; - for (ResourceEntry entry : entries) { + for (final ResourceEntry entry : entries) { if (progress.isCanceled()) { break; } diff --git a/src/org/infinity/search/StringReferenceSearcher.java b/src/org/infinity/search/StringReferenceSearcher.java index 6b628644f..8e935af87 100644 --- a/src/org/infinity/search/StringReferenceSearcher.java +++ b/src/org/infinity/search/StringReferenceSearcher.java @@ -41,7 +41,7 @@ public final class StringReferenceSearcher extends AbstractReferenceSearcher { /** Array of resource extensions which can contains string references. */ public static final String[] FILE_TYPES = { "2DA", "ARE", "BCS", "BS", "CHR", "CHU", "CRE", "DLG", "EFF", "GAM", - "INI", "ITM", "SPL", "SRC", "STO", "TOH", "WMP" }; + "INI", "ITM", "LUA", "MENU", "PRO", "SPL", "SRC", "STO", "TOH", "WMP" }; /** Searched string reference value. */ private final int searchvalue; diff --git a/src/org/infinity/updater/UpdateCheck.java b/src/org/infinity/updater/UpdateCheck.java index 34d6cbdee..750f1ef3e 100644 --- a/src/org/infinity/updater/UpdateCheck.java +++ b/src/org/infinity/updater/UpdateCheck.java @@ -161,7 +161,11 @@ public void actionPerformed(ActionEvent e) { } bDownload.addActionListener(getListeners()); - bDownload.setEnabled(getUpdateInfo().getRelease().htmlUrl != null); + final URL downloadUrl = getUpdateInfo().getRelease().htmlUrl; + if (downloadUrl != null) { + bDownload.setToolTipText(downloadUrl.toExternalForm()); + } + bDownload.setEnabled(downloadUrl != null); bCancel.addActionListener(getListeners()); @@ -297,7 +301,7 @@ public void actionPerformed(ActionEvent e) { private void download() { boolean bRet = false; try { - String link = getUpdateInfo().getRelease().getDefaultAsset().browserDownloadUrl.toExternalForm(); + String link = getUpdateInfo().getRelease().htmlUrl.toExternalForm(); if (!Utils.isUrlValid(link)) { link = getUpdateInfo().getRelease().htmlUrl.toExternalForm(); if (!Utils.isUrlValid(link)) { diff --git a/src/org/infinity/util/IconCache.java b/src/org/infinity/util/IconCache.java index d8bd02ea0..7c5f43302 100644 --- a/src/org/infinity/util/IconCache.java +++ b/src/org/infinity/util/IconCache.java @@ -9,6 +9,7 @@ import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.image.BufferedImage; +import java.io.FileNotFoundException; import java.util.HashMap; import javax.swing.Icon; @@ -18,8 +19,8 @@ import org.infinity.resource.ResourceFactory; import org.infinity.resource.graphics.BamDecoder; import org.infinity.resource.graphics.BamDecoder.BamControl; +import org.infinity.resource.graphics.BmpDecoder; import org.infinity.resource.graphics.ColorConvert; -import org.infinity.resource.graphics.GraphicsResource; import org.infinity.resource.key.ResourceEntry; /** @@ -63,6 +64,22 @@ public static synchronized Icon getDefaultIcon(int size) { k -> new ImageIcon(ColorConvert.createCompatibleImage(size, size, Transparency.BITMASK))); } + /** Returns the {@link Image} stored by the given {@link Icon} instance. */ + public static synchronized Image getIconImage(Icon icon) { + Image retVal = null; + + if (icon instanceof ImageIcon) { + retVal = ((ImageIcon) icon).getImage(); + } else if (icon != null) { + retVal = ColorConvert.createCompatibleImage(icon.getIconWidth(), icon.getIconHeight(), Transparency.TRANSLUCENT); + Graphics2D g = ((BufferedImage) retVal).createGraphics(); + icon.paintIcon(null, g, 0, 0); + g.dispose(); + } + + return retVal; + } + /** Removes all icons associated with the specified BAM {@link ResourceEntry}. */ public static synchronized void remove(ResourceEntry entry) { if (entry != null) { @@ -84,6 +101,18 @@ public static synchronized void clearCache() { * @return {@link Icon} from the specified graphics resource. Returns {@code null} if icon is not available. */ public static synchronized Icon getIcon(ResourceEntry entry, int size) { + return getIcon(entry, size, null); + } + + /** + * Returns the icon associated with the specified graphics {@link ResourceEntry} scaled to the specified size. + * + * @param entry {@link ResourceEntry} of a supported graphics resource. Currently supported: BAM, BMP. + * @param size Width and height of the resulting icon, in pixels. + * @param defIcon Use this icon resource if the specified graphics resource is {@code null}. + * @return {@link Icon} from the specified graphics resource. Returns {@code null} if icon is not available. + */ + public static synchronized Icon getIcon(ResourceEntry entry, int size, Icon defIcon) { if (size < MIN_SIZE) { return null; } @@ -108,7 +137,11 @@ public static synchronized Icon getIcon(ResourceEntry entry, int size) { } if (retVal == null) { - retVal = getDefaultIcon(size); + if (defIcon != null) { + retVal = defIcon; + } else { + retVal = getDefaultIcon(size); + } } return retVal; @@ -122,18 +155,37 @@ public static synchronized Icon getIcon(ResourceEntry entry, int size) { * @return {@link Icon} associated with the specified game resource. Returns {@code null} if icon is not available. */ public static synchronized Icon get(ResourceEntry entry, int size) { + return get(entry, size, null); + } + + /** + * Returns the icon associated with the specified {@link ResourceEntry} scaled to the specified size. + * + * @param entry {@link ResourceEntry} of a supported game resource. Currently supported: ITM, SPL, BAM, BMP. + * @param size Width and height of the resulting icon, in pixels. + * @param defIcon Use this icon resource if the specified game resource doesn't provide an icon definition. + * @return {@link Icon} associated with the specified game resource. Returns {@code null} if icon is not available. + */ + public static synchronized Icon get(ResourceEntry entry, int size, Icon defIcon) { ResourceEntry graphicsEntry = null; if (entry != null) { final String ext = entry.getExtension().toUpperCase(); if (ext.equals("ITM") || ext.equals("SPL")) { - graphicsEntry = ResourceFactory.getResourceIcon(entry); + try { + graphicsEntry = ResourceFactory.getResourceIcon(entry); + if (graphicsEntry == null && defIcon == null) { + defIcon = entry.getIcon(); + } + } catch (FileNotFoundException e) { + defIcon = getDefaultIcon(size); + } } else if (ext.equals("BAM") || ext.equals("BMP")) { graphicsEntry = entry; } } - return getIcon(graphicsEntry, size); + return getIcon(graphicsEntry, size, defIcon); } /** Returns the cached icon for the specified BAM {@link ResourceEntry} and {@code size}. */ @@ -243,10 +295,10 @@ private static synchronized Image getBmpImage(ResourceEntry bmpEntry) { if (bmpEntry != null) { try { - final GraphicsResource res = new GraphicsResource(bmpEntry); - retVal = res.getImage(); + final BmpDecoder decoder = BmpDecoder.loadBmp(bmpEntry); + retVal = decoder.getImage(); } catch (Exception e) { - e.printStackTrace(); + // No log output; catches lots of false positives } }