From f18ceab4008fb681588b4fe0d7dcfce21b3007bf Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:46:05 +0200 Subject: [PATCH] Look up ITEMTYPE.2DA for item categories in EE games - use specialized data type to display item categories - use ITEMTYPE.2DA definitions for item category list in EE games - add associated slot type to category label if available - minor improvements to symbol lookup in IdsMapEntry class --- src/org/infinity/datatype/ItemTypeBitmap.java | 139 ++++++++++++++++++ .../infinity/resource/effects/Opcode181.java | 6 +- .../infinity/resource/effects/Opcode183.java | 7 +- .../infinity/resource/itm/ItmResource.java | 26 +--- src/org/infinity/resource/sto/Purchases.java | 12 +- src/org/infinity/search/SearchResource.java | 12 +- src/org/infinity/util/IdsMapEntry.java | 33 ++++- src/org/infinity/util/Table2da.java | 4 +- src/org/infinity/util/Table2daCache.java | 20 +++ 9 files changed, 212 insertions(+), 47 deletions(-) create mode 100644 src/org/infinity/datatype/ItemTypeBitmap.java diff --git a/src/org/infinity/datatype/ItemTypeBitmap.java b/src/org/infinity/datatype/ItemTypeBitmap.java new file mode 100644 index 000000000..d60efad59 --- /dev/null +++ b/src/org/infinity/datatype/ItemTypeBitmap.java @@ -0,0 +1,139 @@ +// 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 java.util.Locale; +import java.util.TreeMap; + +import org.infinity.resource.Profile; +import org.infinity.resource.ResourceFactory; +import org.infinity.util.IdsMap; +import org.infinity.util.IdsMapCache; +import org.infinity.util.IdsMapEntry; +import org.infinity.util.Misc; +import org.infinity.util.Table2da; +import org.infinity.util.Table2daCache; + +/** + * Specialized {@link HashBitmap} that uses a mix of hardcoded entries and custom entries from ITEMTYPE.2DA + * if available. + */ +public class ItemTypeBitmap extends HashBitmap { + private static final String TABLE_NAME = "ITEMTYPE.2DA"; + + public static final String[] CATEGORIES_ARRAY = { "Miscellaneous", "Amulets and necklaces", "Armor", + "Belts and girdles", "Boots", "Arrows", "Bracers and gauntlets", "Headgear", "Keys", "Potions", "Rings", + "Scrolls", "Shields", "Food", "Bullets", "Bows", "Daggers", "Maces", "Slings", "Small swords", "Large swords", + "Hammers", "Morning stars", "Flails", "Darts", "Axes", "Quarterstaves", "Crossbows", "Hand-to-hand weapons", + "Spears", "Halberds", "Bolts", "Cloaks and robes", "Gold pieces", "Gems", "Wands", "Containers", "Books", + "Familiars", "Tattoos", "Lenses", "Bucklers", "Candles", "Child bodies", "Clubs", "Female bodies", "Keys (old)", + "Large shields", "Male bodies", "Medium shields", "Notes", "Rods", "Skulls", "Small shields", "Spider bodies", + "Telescopes", "Bottles", "Greatswords", "Bags", "Furs and pelts", "Leather armor", "Studded leather", + "Chain mail", "Splint mail", "Plate mail", "Full plate", "Hide armor", "Robes", "Scale mail", "Bastard swords", + "Scarves", "Rations", "Hats", "Gloves", "Eyeballs", "Earrings", "Teeth", "Bracelets" }; + + public static final String[] CATEGORIES11_ARRAY = { "Miscellaneous", "Amulets and necklaces", "Armor", + "Belts and girdles", "Boots", "Arrows", "Bracers and gauntlets", "Headgear", "Keys", "Potions", "Rings", + "Scrolls", "Shields", "Spells", "Bullets", "Bows", "Daggers", "Maces", "Slings", "Small swords", "Large swords", + "Hammers", "Morning stars", "Flails", "Darts", "Axes", "Quarterstaves", "Crossbows", "Hand-to-hand weapons", + "Greatswords", "Halberds", "Bolts", "Cloaks and robes", "Copper commons", "Gems", "Wands", "Eyeballs", + "Bracelets", "Earrings", "Tattoos", "Lenses", "Teeth" }; + + private static TreeMap CATEGORIES = null; + + public ItemTypeBitmap(ByteBuffer buffer, int offset, int length, String name) { + super(buffer, offset, length, name, getItemCategories(), false, false); + } + + /** + * Returns a list of available item categories. List entries depend on the detected game and may include + * static and dynamic elements. + * + * @return Array of strings with item categories. + */ + public static TreeMap getItemCategories() { + synchronized (TABLE_NAME) { + if (Profile.isEnhancedEdition() && !Table2daCache.isCached(TABLE_NAME)) { + CATEGORIES = null; + } + if (CATEGORIES == null) { + CATEGORIES = buildCategories(); + } + } + return CATEGORIES; + } + + /** Rebuilds the list of item categories. */ + private static TreeMap buildCategories() { + final TreeMap retVal = new TreeMap<>(); + if (Profile.isEnhancedEdition() && ResourceFactory.resourceExists(TABLE_NAME)) { + final IdsMap slots = IdsMapCache.get("SLOTS.IDS"); + final Table2da table = Table2daCache.get(TABLE_NAME); + final String baseName = "Extra category "; + int baseIndex = 1; + for (int row = 0, count = table.getRowCount(); row < count; row++) { + final String idxValue = table.get(row, 0); + final int radix = idxValue.startsWith("0x") ? 16 : 10; + String catName = ""; + try { + int idx = Integer.parseInt(idxValue, radix); + if (idx >= 0 && idx < CATEGORIES_ARRAY.length) { + // looking up hardcoded category name + catName = CATEGORIES_ARRAY[idx]; + } else { + // generating custom category name + catName = baseName + baseIndex; + baseIndex++; + } + + // adding slot restriction if available + int slot = Misc.toNumber(table.get(row, 3), -1); + if (slot >= 0) { + final IdsMapEntry slotEntry = slots.get(slot); + if (slotEntry != null) { + final String slotName = beautifyString(slotEntry.getSymbol(), "SLOT"); + if (slotName != null && !slotName.isEmpty()) { + catName = catName + " [" + slotName + " slot" + "]"; + } + } + } + + retVal.put((long) idx, catName); + } catch (NumberFormatException e) { + // skip entry with log message + System.err.printf("%s: Invalid index at row=%d (value=%s)\n", TABLE_NAME, row, idxValue); + } + } + } else if (Profile.getEngine() == Profile.Engine.PST) { + // PST + for (long idx = 0, count = CATEGORIES11_ARRAY.length; idx < count; idx++) { + retVal.put(idx, CATEGORIES11_ARRAY[(int) idx]); + } + } else { + // Any non-EE games except PST + for (long idx = 0, count = CATEGORIES_ARRAY.length; idx < count; idx++) { + retVal.put(idx, CATEGORIES_ARRAY[(int) idx]); + } + } + + return retVal; + } + + private static String beautifyString(String s, String removedPrefix) { + String retVal = s; + if (retVal != null) { + retVal = retVal.replaceFirst(removedPrefix + "_?", ""); + final String[] words = retVal.split("[ _]+"); + for (int i = 0; i < words.length; i++) { + words[i] = words[i].charAt(0) + words[i].substring(1).toLowerCase(Locale.ENGLISH); + } + retVal = String.join(" ", words); + retVal = retVal.replaceFirst("(\\D)(\\d)", "$1 $2"); + retVal = retVal.trim(); + } + return retVal; + } +} diff --git a/src/org/infinity/resource/effects/Opcode181.java b/src/org/infinity/resource/effects/Opcode181.java index 3a570ace8..74a284f70 100644 --- a/src/org/infinity/resource/effects/Opcode181.java +++ b/src/org/infinity/resource/effects/Opcode181.java @@ -9,10 +9,10 @@ import org.infinity.datatype.Bitmap; import org.infinity.datatype.Datatype; +import org.infinity.datatype.ItemTypeBitmap; import org.infinity.datatype.StringRef; import org.infinity.resource.Profile; import org.infinity.resource.StructEntry; -import org.infinity.resource.itm.ItmResource; /** * Implemention of opcode 181. @@ -41,14 +41,14 @@ public Opcode181() { protected String makeEffectParamsGeneric(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new StringRef(buffer, offset, EFFECT_DESC_NOTE)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_ITEM_TYPE, ItmResource.CATEGORIES_ARRAY)); + list.add(new ItemTypeBitmap(buffer, offset + 4, 4, EFFECT_ITEM_TYPE)); return null; } @Override protected String makeEffectParamsEE(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { - list.add(new Bitmap(buffer, offset, 4, EFFECT_ITEM_TYPE, ItmResource.CATEGORIES_ARRAY)); + list.add(new ItemTypeBitmap(buffer, offset, 4, EFFECT_ITEM_TYPE)); list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_RESTRICTION, RESTRICTION_TYPES_EE)); return null; } diff --git a/src/org/infinity/resource/effects/Opcode183.java b/src/org/infinity/resource/effects/Opcode183.java index 50f7fabf1..e4b22e826 100644 --- a/src/org/infinity/resource/effects/Opcode183.java +++ b/src/org/infinity/resource/effects/Opcode183.java @@ -7,14 +7,13 @@ 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.ItemTypeBitmap; import org.infinity.datatype.StringRef; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Profile; import org.infinity.resource.StructEntry; -import org.infinity.resource.itm.ItmResource; /** * Implemention of opcode 183. @@ -41,7 +40,7 @@ public Opcode183() { 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_ITEM_TYPE, ItmResource.CATEGORIES_ARRAY)); + list.add(new ItemTypeBitmap(buffer, offset + 4, 4, EFFECT_ITEM_TYPE)); return RES_TYPE; } @@ -49,7 +48,7 @@ protected String makeEffectParamsGeneric(Datatype parent, ByteBuffer buffer, int protected String makeEffectParamsIWD2(Datatype parent, ByteBuffer buffer, int offset, List list, boolean isVersion1) { list.add(new StringRef(buffer, offset, EFFECT_STRING)); - list.add(new Bitmap(buffer, offset + 4, 4, EFFECT_ITEM_TYPE, ItmResource.CATEGORIES_ARRAY)); + list.add(new ItemTypeBitmap(buffer, offset + 4, 4, EFFECT_ITEM_TYPE)); return RES_TYPE; } diff --git a/src/org/infinity/resource/itm/ItmResource.java b/src/org/infinity/resource/itm/ItmResource.java index d20e9f4ce..cc1dd0e10 100644 --- a/src/org/infinity/resource/itm/ItmResource.java +++ b/src/org/infinity/resource/itm/ItmResource.java @@ -15,12 +15,12 @@ import javax.swing.JComponent; import javax.swing.JScrollPane; -import org.infinity.datatype.Bitmap; import org.infinity.datatype.ColorValue; import org.infinity.datatype.DecNumber; import org.infinity.datatype.Flag; import org.infinity.datatype.IdsBitmap; import org.infinity.datatype.IsNumeric; +import org.infinity.datatype.ItemTypeBitmap; import org.infinity.datatype.ResourceRef; import org.infinity.datatype.SectionCount; import org.infinity.datatype.SectionOffset; @@ -111,24 +111,6 @@ public final class ItmResource extends AbstractStruct implements Resource, HasCh public static final String ITM_SPEAKER_NAME = "Speaker name"; public static final String ITM_WEAPON_COLOR = "Weapon color"; - public static final String[] CATEGORIES_ARRAY = { "Miscellaneous", "Amulets and necklaces", "Armor", - "Belts and girdles", "Boots", "Arrows", "Bracers and gauntlets", "Headgear", "Keys", "Potions", "Rings", - "Scrolls", "Shields", "Food", "Bullets", "Bows", "Daggers", "Maces", "Slings", "Small swords", "Large swords", - "Hammers", "Morning stars", "Flails", "Darts", "Axes", "Quarterstaves", "Crossbows", "Hand-to-hand weapons", - "Spears", "Halberds", "Bolts", "Cloaks and robes", "Gold pieces", "Gems", "Wands", "Containers", "Books", - "Familiars", "Tattoos", "Lenses", "Bucklers", "Candles", "Child bodies", "Clubs", "Female bodies", "Keys (old)", - "Large shields", "Male bodies", "Medium shields", "Notes", "Rods", "Skulls", "Small shields", "Spider bodies", - "Telescopes", "Bottles", "Greatswords", "Bags", "Furs and pelts", "Leather armor", "Studded leather", - "Chain mail", "Splint mail", "Plate mail", "Full plate", "Hide armor", "Robes", "Scale mail", "Bastard swords", - "Scarves", "Rations", "Hats", "Gloves", "Eyeballs", "Earrings", "Teeth", "Bracelets" }; - - public static final String[] CATEGORIES11_ARRAY = { "Miscellaneous", "Amulets and necklaces", "Armor", - "Belts and girdles", "Boots", "Arrows", "Bracers and gauntlets", "Headgear", "Keys", "Potions", "Rings", - "Scrolls", "Shields", "Spells", "Bullets", "Bows", "Daggers", "Maces", "Slings", "Small swords", "Large swords", - "Hammers", "Morning stars", "Flails", "Darts", "Axes", "Quarterstaves", "Crossbows", "Hand-to-hand weapons", - "Greatswords", "Halberds", "Bolts", "Cloaks and robes", "Copper commons", "Gems", "Wands", "Eyeballs", - "Bracelets", "Earrings", "Tattoos", "Lenses", "Teeth" }; - public static final String[] FLAGS_ARRAY = { "None", "Critical item", "Two-handed", "Droppable", "Displayable", "Cursed", "Not copyable", "Magical", "Left-handed", "Silver", "Cold iron", "Off-handed", "Conversable", "EE: Fake two-handed", "EE: Forbid off-hand weapon", "", "EE: Adamantine", null, null, null, null, null, null, @@ -352,10 +334,10 @@ public int read(ByteBuffer buffer, int offset) throws Exception { addField(new ResourceRef(buffer, 16, ITM_DROP_SOUND, "WAV")); if (Profile.getGame() == Profile.Game.PSTEE) { addField(new Flag(buffer, 24, 4, ITM_FLAGS, FLAGS_PSTEE_ARRAY)); - addField(new Bitmap(buffer, 28, 2, ITM_CATEGORY, CATEGORIES_ARRAY)); + addField(new ItemTypeBitmap(buffer, 28, 2, ITM_CATEGORY)); } else { addField(new Flag(buffer, 24, 4, ITM_FLAGS, FLAGS11_ARRAY)); - addField(new Bitmap(buffer, 28, 2, ITM_CATEGORY, CATEGORIES11_ARRAY)); + addField(new ItemTypeBitmap(buffer, 28, 2, ITM_CATEGORY)); } addField(new Flag(buffer, 30, 4, ITM_UNUSABLE_BY, USABILITY11_ARRAY)); addField(new TextBitmap(buffer, 34, 2, ITM_EQUIPPED_APPEARANCE, Profile.getEquippedAppearanceMap())); @@ -363,7 +345,7 @@ public int read(ByteBuffer buffer, int offset) throws Exception { addField(new ResourceRef(buffer, 16, ITM_USED_UP_ITEM, "ITM")); addField( new Flag(buffer, 24, 4, ITM_FLAGS, IdsMapCache.getUpdatedIdsFlags(FLAGS_ARRAY, "ITEMFLAG.IDS", 4, false, false))); - addField(new Bitmap(buffer, 28, 2, ITM_CATEGORY, CATEGORIES_ARRAY)); + addField(new ItemTypeBitmap(buffer, 28, 2, ITM_CATEGORY)); if (isV20) { addField(new Flag(buffer, 30, 4, ITM_UNUSABLE_BY, USABILITY20_ARRAY)); } else { diff --git a/src/org/infinity/resource/sto/Purchases.java b/src/org/infinity/resource/sto/Purchases.java index d76ef2db4..7342c8099 100644 --- a/src/org/infinity/resource/sto/Purchases.java +++ b/src/org/infinity/resource/sto/Purchases.java @@ -6,24 +6,20 @@ import java.nio.ByteBuffer; -import org.infinity.datatype.Bitmap; +import org.infinity.datatype.ItemTypeBitmap; import org.infinity.resource.AddRemovable; -import org.infinity.resource.Profile; -import org.infinity.resource.itm.ItmResource; import org.infinity.util.io.StreamUtils; -public final class Purchases extends Bitmap implements AddRemovable { +public final class Purchases extends ItemTypeBitmap implements AddRemovable { // STO/Purchases-specific field labels public static final String STO_PURCHASES = "Store purchases"; Purchases() { - super(StreamUtils.getByteBuffer(4), 0, 4, STO_PURCHASES, - (Profile.getEngine() == Profile.Engine.PST) ? ItmResource.CATEGORIES11_ARRAY : ItmResource.CATEGORIES_ARRAY); + super(StreamUtils.getByteBuffer(4), 0, 4, STO_PURCHASES); } Purchases(ByteBuffer buffer, int offset, int number) { - super(buffer, offset, 4, STO_PURCHASES + " " + number, - (Profile.getEngine() == Profile.Engine.PST) ? ItmResource.CATEGORIES11_ARRAY : ItmResource.CATEGORIES_ARRAY); + super(buffer, offset, 4, STO_PURCHASES + " " + number); } // --------------------- Begin Interface AddRemovable --------------------- diff --git a/src/org/infinity/search/SearchResource.java b/src/org/infinity/search/SearchResource.java index cc6ee3f04..edbce953f 100644 --- a/src/org/infinity/search/SearchResource.java +++ b/src/org/infinity/search/SearchResource.java @@ -61,6 +61,7 @@ import org.infinity.NearInfinity; import org.infinity.datatype.IdsBitmap; import org.infinity.datatype.IsNumeric; +import org.infinity.datatype.ItemTypeBitmap; import org.infinity.datatype.KitIdsBitmap; import org.infinity.datatype.PriTypeBitmap; import org.infinity.datatype.ProRef; @@ -2030,17 +2031,17 @@ private void init() { String[] sCat; if ((Boolean) Profile.getProperty(Profile.Key.IS_SUPPORTED_ITM_V11)) { sFlags = ItmResource.FLAGS11_ARRAY; - sCat = ItmResource.CATEGORIES11_ARRAY; + sCat = ItemTypeBitmap.CATEGORIES11_ARRAY; } else if ((Boolean) Profile.getProperty(Profile.Key.IS_SUPPORTED_ITM_V20)) { sFlags = ItmResource.FLAGS_ARRAY; - sCat = ItmResource.CATEGORIES_ARRAY; + sCat = ItemTypeBitmap.CATEGORIES_ARRAY; } else { if (Profile.getGame() == Profile.Game.PSTEE) { sFlags = ItmResource.FLAGS_PSTEE_ARRAY; } else { sFlags = ItmResource.FLAGS_ARRAY; } - sCat = ItmResource.CATEGORIES_ARRAY; + sCat = ItemTypeBitmap.CATEGORIES_ARRAY; } pFlags = new FlagsPanel(4, sFlags); @@ -5425,8 +5426,9 @@ private void init() { cbLabel[i] = new JCheckBox(String.format("Category %d:", i + 1)); cbLabel[i].addActionListener(this); - String[] cat = ((Boolean) Profile.getProperty(Profile.Key.IS_SUPPORTED_STO_V11)) ? ItmResource.CATEGORIES11_ARRAY - : ItmResource.CATEGORIES_ARRAY; + String[] cat = ((Boolean) Profile.getProperty(Profile.Key.IS_SUPPORTED_STO_V11)) + ? ItemTypeBitmap.CATEGORIES11_ARRAY + : ItemTypeBitmap.CATEGORIES_ARRAY; cbCategory[i] = new AutoComboBox<>(IndexedString.createArray(cat, 0, 0)); } diff --git a/src/org/infinity/util/IdsMapEntry.java b/src/org/infinity/util/IdsMapEntry.java index e55a234d4..2ad7aff0e 100644 --- a/src/org/infinity/util/IdsMapEntry.java +++ b/src/org/infinity/util/IdsMapEntry.java @@ -33,9 +33,36 @@ public int getNumSymbols() { return symbols.size(); } - /** Returns the most recently added symbolic name. */ + /** Returns the first available symbolic name. */ public String getSymbol() { - return symbols.peek(); + return symbols.peekFirst(); + } + + /** Returns the most recently added symbolic name. */ + public String getLastSymbol() { + return symbols.peekLast(); + } + + /** + * Returns the symbolic name at the specified index. Call {@link #getNumSymbols()} to get the number of available + * symbolic names. + * + * @param index Index of the symbolic name. + * @return Symbolic name as string. + * @throws IndexOutOfBoundsException if {@code index} is out of bounds. + */ + public String getSymbol(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= symbols.size()) { + throw new IndexOutOfBoundsException("Index out of bounds: " + index); + } + + final Iterator iter = symbols.iterator(); + String retVal = null; + while (index >= 0) { + retVal = iter.next(); + index--; + } + return retVal; } /** Returns an iterator over the whole collection of available symbols. */ @@ -51,7 +78,7 @@ public void addSymbol(String symbol) { } if (!symbol.isEmpty() && !symbols.contains(symbol)) { - symbols.push(symbol); + symbols.add(symbol); } } diff --git a/src/org/infinity/util/Table2da.java b/src/org/infinity/util/Table2da.java index 2c6cce285..fdc2a99f8 100644 --- a/src/org/infinity/util/Table2da.java +++ b/src/org/infinity/util/Table2da.java @@ -53,7 +53,7 @@ public void reload() { init(entry); } - /** Returns total number of columns, including header column. */ + /** Returns total number of data columns. */ public int getColCount() { return table.isEmpty() ? header.size() : columnCount; } @@ -66,7 +66,7 @@ public int getColCount(int row) { return 0; } - /** Returns number of rows, including header row. */ + /** Returns number of data rows. */ public int getRowCount() { return table.size(); } diff --git a/src/org/infinity/util/Table2daCache.java b/src/org/infinity/util/Table2daCache.java index fdad68077..8458d69e5 100644 --- a/src/org/infinity/util/Table2daCache.java +++ b/src/org/infinity/util/Table2daCache.java @@ -24,6 +24,26 @@ public static synchronized void clearCache() { MAP.clear(); } + /** + * Returns whether the specified 2DA resource has already been cached. + * + * @param resource 2DA resource name. + * @return {@code true} if the resource has been cached, {@code false} otherwise. + */ + public static boolean isCached(String resource) { + return isCached(ResourceFactory.getResourceEntry(resource)); + } + + /** + * Returns whether the specified 2DA resource has already been cached. + * + * @param entry 2DA resource entry. + * @return {@code true} if the resource has been cached, {@code false} otherwise. + */ + public static boolean isCached(ResourceEntry entry) { + return (entry != null && MAP.containsKey(entry)); + } + /** * Returns a Table2da object based on the specified 2DA resource. *