// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.datatype;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.infinity.resource.AbstractStruct;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.StructEntry;
import org.infinity.util.IdsMap;
import org.infinity.util.IdsMapCache;
import org.infinity.util.IdsMapEntry;
import org.infinity.util.LongIntegerHashMap;
import org.infinity.util.Table2da;
import org.infinity.util.Table2daCache;
/**
* Specialized Bitmap type for translating SPLPROT.2DA data into human-readable descriptions.
*/
public class SpellProtType extends Bitmap
{
public static final String DEFAULT_NAME_TYPE = "Creature type";
public static final String DEFAULT_NAME_VALUE = "Creature value";
public static final String DEFAULT_NAME_UNUSED = "Unused";
public static final String[] s_relation = { "<=", "=", "<", ">", ">=", "!=",
"bit_l_e", "bit_g_e", "bit_eq",
"bit_uneq", "bit_greater", "bit_less" };
// TODO: remove this array after all Enhanced Editions have been updated
public static final String[] s_cretype_ee = {
// 0..9
"Anyone", "Undead", "Not undead", "Fire-dwelling", "Not fire-dwelling", "Humanoid",
"Not humanoid", "Animal", "Not animal", "Elemental",
// 10..19
"Not elemental", "Fungus", "Not fungus", "Huge creature", "Not huge creature", "Elf", "Not elf",
"Umber hulk", "Not umber hulk", "Half-elf",
// 20..29
"Not half-elf", "Humanoid or animal", "Not humanoid or animal", "Blind", "Not blind",
"Cold-dwelling", "Not cold-dwelling", "Golem", "Not golem", "Minotaur",
// 30..39
"Not minotaur", "Undead or fungus", "Not undead or fungus", "Good", "Not good", "Neutral",
"Not neutral", "Evil", "Not evil", "Paladin",
// 40..49
"Not paladin", "Same moral alignment as source", "Not same moral alignment as source", "Source",
"Not source", "Water-dwelling", "Not water-dwelling", "Breathing", "Not breathing", "Allies",
// 50..59
"Not allies", "Enemies", "Not enemies", "Fire or cold dwelling", "Not fire or cold dwelling",
"Unnatural", "Not unnatural", "Male", "Not male", "Lawful",
// 60..69
"Not lawful", "Chaotic", "Not chaotic", "Evasion check", "Orc", "Not orc", "Deaf", "Not deaf",
"", "",
// 70..79
"", "", "", "", "", "", "", "", "", "",
// 80..89
"", "", "", "", "", "", "", "", "", "",
// 90..99
"", "", "", "", "", "", "", "", "", "",
// 100..109
"", "", "EA.IDS", "GENERAL.IDS", "RACE.IDS", "CLASS.IDS", "SPECIFIC.IDS", "GENDER.IDS",
"ALIGN.IDS", "KIT.IDS" };
private static final String tableName = "SPLPROT.2DA";
private static final LongIntegerHashMap<String> statIds = new LongIntegerHashMap<String>();
private static String[] creType;
static {
statIds.put(Long.valueOf(152L), "KIT.IDS");
statIds.put(Long.valueOf(0x106L), "AREATYPE.IDS");
// statIds.put(Long.valueOf(0x107L), "TIMEODAY.IDS");
statIds.put(Long.valueOf(0x10aL), "EA.IDS");
statIds.put(Long.valueOf(0x10bL), "GENERAL.IDS");
statIds.put(Long.valueOf(0x10cL), "RACE.IDS");
statIds.put(Long.valueOf(0x10dL), "CLASS.IDS");
statIds.put(Long.valueOf(0x10eL), "SPECIFIC.IDS");
statIds.put(Long.valueOf(0x10fL), "GENDER.IDS");
statIds.put(Long.valueOf(0x110L), "ALIGNMEN.IDS");
statIds.put(Long.valueOf(0x111L), "STATE.IDS");
statIds.put(Long.valueOf(0x112L), "SPLSTATE.IDS");
}
private final int index;
private final boolean isExternalized;
private boolean updateIdsValues; // defines whether to update the "Creature value" field automatically
public SpellProtType(ByteBuffer buffer, int offset, int length)
{
this(null, buffer, offset, length, null, -1);
}
public SpellProtType(StructEntry parent, ByteBuffer buffer, int offset, int length)
{
this(parent, buffer, offset, length, null, -1);
}
public SpellProtType(ByteBuffer buffer, int offset, int length, String name)
{
this(null, buffer, offset, length, name, -1);
}
public SpellProtType(StructEntry parent, ByteBuffer buffer, int offset, int length, String name)
{
this(parent, buffer, offset, length, name, -1);
}
public SpellProtType(ByteBuffer buffer, int offset, int length, String name, int idx)
{
this(null, buffer, offset, length, name, idx);
}
public SpellProtType(StructEntry parent, ByteBuffer buffer, int offset, int length, String name, int idx)
{
super(parent, buffer, offset, length, createFieldName(name, idx, DEFAULT_NAME_TYPE), getTypeTable());
this.index = idx;
this.isExternalized = isTableExternalized();
this.updateIdsValues = true;
}
//--------------------- Begin Interface Editable ---------------------
@Override
public boolean updateValue(AbstractStruct struct)
{
boolean retVal = super.updateValue(struct);
if (updateIdsValues && retVal) {
int valueOffset = getOffset() - getSize();
List<StructEntry> list = struct.getList();
for (int i = 0, size = list.size(); i < size; i++) {
StructEntry entry = list.get(i);
if (entry.getOffset() == valueOffset && entry instanceof Datatype) {
ByteBuffer buffer = ((Datatype)entry).getDataBuffer();
StructEntry newEntry = createCreatureValueFromType(buffer, 0);
newEntry.setOffset(valueOffset);
list.set(i, newEntry);
// notifying listeners
struct.fireTableRowsUpdated(i, i);
}
}
}
return retVal;
}
//--------------------- End Interface Editable ---------------------
public StructEntry createCreatureValueFromType(ByteBuffer buffer)
{
return createCreatureValueFromType(buffer, getOffset() - getSize(), getSize(), null);
}
public StructEntry createCreatureValueFromType(ByteBuffer buffer, int offset)
{
return createCreatureValueFromType(buffer, offset, getSize(), null);
}
public StructEntry createCreatureValueFromType(ByteBuffer buffer, int offset, int size, String name)
{
if (useCustomValue()) {
String idsFile = getIdsFile();
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));
}
}
return new DecNumber(buffer, offset, size, createFieldName(name, index, DEFAULT_NAME_UNUSED));
}
/** Returns whether this SpellProtType instance automatically updates the associated creature value field. */
public boolean isUpdatingCreatureValues()
{
return updateIdsValues;
}
/** Specify whether this SpellProtType instance should automatically update the associated creature value field. */
public void setUpdateCreatureValues(boolean b)
{
updateIdsValues = b;
}
/** Returns whether this datatype makes use of the externalized table for creature types. */
public boolean isExternalized()
{
return isExternalized;
}
/** Returns true if the specified creature type value depends on a user-defined value. */
public boolean useCustomValue()
{
int value = getValue();
if (isExternalized()) {
Table2da table = Table2daCache.get(tableName);
if (table != null) {
return (-1 == toNumber(table.get(value, 2), 0));
}
} else {
if (value >= 0 && value < s_cretype_ee.length) {
return s_cretype_ee[value].endsWith(".IDS");
}
}
return false;
}
/** Returns the IDS resource name use by the specified creature type. Returns an empty string if unused. */
public String getIdsFile()
{
int value = getValue();
if (isExternalized()) {
Table2da table = Table2daCache.get(tableName);
if (table != null) {
int id = toNumber(table.get(value, 1), -1);
String retVal = statIds.get(Long.valueOf((long)id));
if (retVal != null) {
return retVal;
}
}
} else {
if (value >= 0 && value < s_cretype_ee.length) {
if (s_cretype_ee[value].endsWith(".IDS")) {
return s_cretype_ee[value];
}
}
}
return "";
}
/** Returns whether creature table has been externalized into a 2DA file. */
public static boolean isTableExternalized()
{
if (Profile.isEnhancedEdition()) {
return ResourceFactory.resourceExists(tableName);
} else {
return false;
}
}
/** Returns name of the 2DA resource used as reference for the list. */
public static String getTableName()
{
return tableName;
}
/** Returns true if the specified creature type value depends on a user-defined value. */
public static boolean useCustomValue(int value)
{
if (isTableExternalized()) {
Table2da table = Table2daCache.get(tableName);
if (table != null) {
return (-1 == toNumber(table.get(value, 2), 0));
}
} else {
if (value >= 0 && value < s_cretype_ee.length) {
return s_cretype_ee[value].endsWith(".IDS");
}
}
return false;
}
/** Returns the IDS resource name use by the specified creature type. Returns an empty string if unused. */
public static String getIdsFile(int value)
{
if (isTableExternalized()) {
Table2da table = Table2daCache.get(tableName);
if (table != null) {
boolean isCustom = (-1 == toNumber(table.get(value, 2), 0));
if (isCustom) {
int id = toNumber(table.get(value, 1), -1);
String retVal = statIds.get(Long.valueOf((long)id));
if (retVal != null) {
return retVal;
}
}
}
} else if (value >= 0 && value < s_cretype_ee.length) {
if (s_cretype_ee[value].endsWith(".IDS")) {
return s_cretype_ee[value];
}
}
return "";
}
public static String[] getTypeTable()
{
if (creType == null) {
if (isTableExternalized()) {
creType = getExternalizedTypeTable();
} else {
creType = Arrays.copyOf(s_cretype_ee, s_cretype_ee.length);
}
}
return creType;
}
public static void resetTypeTable()
{
creType = null;
Table2daCache.cacheInvalid(ResourceFactory.getResourceEntry(tableName));
}
/** Returns an array of descriptions based on the entries in the 2DA resource. */
private static String[] getExternalizedTypeTable()
{
String[] retVal = null;
if (ResourceFactory.resourceExists(tableName)) {
Table2da table = Table2daCache.get(tableName);
if (table != null) {
retVal = new String[table.getRowCount()];
for (int row = 0, size = table.getRowCount(); row < size; row++) {
String label;
int stat = toNumber(table.get(row, 1), -1);
int value = toNumber(table.get(row, 2), -1);
int rel = toNumber(table.get(row, 3), -1);
switch (stat) {
case 0x100: // source equals target
label = "Source";
break;
case 0x101: // source is not target
label = "Not source";
break;
case 0x102: // circle size
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("Circle size %s %d [0x%x]", getRelation(rel), value, value);
} else {
label = String.format("Circle size %s %d", getRelation(rel), value);
}
break;
case 0x103: // use two rows of splprot.2da
label = String.format("Match entries %d or %d", value, rel);
break;
case 0x104: // negate 0x103
label = String.format("Not match entries %d or %d", value, rel);
break;
case 0x105: // source and target morale match
switch (rel) {
case 1: label = "Same morale alignment"; break;
case 5: label = "Not same morale alignment"; break;
default: label = "Morale alignment relation: " + getRelation(rel); break;
}
break;
case 0x106: // areatype (like outdoors, forest, etc)
if (isBitwiseRelation(rel) && value != -1) {
} else {
label = String.format("AREATYPE %s %s [0x%x]",
getRelation(rel),
getIdsValue("AREATYPE.IDS", value, isBitwiseRelation(rel)),
value);
}
label = String.format("AREATYPE %s %s",
getRelation(rel),
getIdsValue("AREATYPE.IDS", value, isBitwiseRelation(rel)));
break;
case 0x107: // daytime
label = String.format("Time of day is %d to %d", value, rel);
break;
case 0x108: // source and target ethical match
switch (rel) {
case 1: label = "Same ethical alignment"; break;
case 5: label = "Not same ethical alignment"; break;
default: label = "Ethical alignment: " + getRelation(rel); break;
}
break;
case 0x109: // evasion
label = "Evasion check";
break;
case 0x10a: // EA
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("EA %s %s [0x%x]",
getRelation(rel),
getIdsValue("EA.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("EA %s %s",
getRelation(rel),
getIdsValue("EA.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x10b: // GENERAL
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("GENERAL %s %s [0x%3$x]",
getRelation(rel),
getIdsValue("GENERAL.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("GENERAL %s %s",
getRelation(rel),
getIdsValue("GENERAL.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x10c: // RACE
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("RACE %s %s [0x%x]",
getRelation(rel),
getIdsValue("RACE.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("RACE %s %s",
getRelation(rel),
getIdsValue("RACE.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x10d: // CLASS
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("CLASS %s %s [0x%x]",
getRelation(rel),
getIdsValue("CLASS.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("CLASS %s %s",
getRelation(rel),
getIdsValue("CLASS.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x10e: // SPECIFIC
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("SPECIFIC %s %s [0x%x]",
getRelation(rel),
getIdsValue("SPECIFIC.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("SPECIFIC %s %s",
getRelation(rel),
getIdsValue("SPECIFIC.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x10f: // GENDER
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("GENDER %s %s [0x%x]",
getRelation(rel),
getIdsValue("GENDER.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("GENDER %s %s",
getRelation(rel),
getIdsValue("GENDER.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x110: // ALIGNMENT
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("ALIGNMENT %s %s [0x%x]",
getRelation(rel),
getIdsValue("ALIGNMEN.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("ALIGNMENT %s %s",
getRelation(rel),
getIdsValue("ALIGNMEN.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x111: // STATE
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("STATE %s %s [0x%x]",
getRelation(rel),
getIdsValue("STATE.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("STATE %s %s",
getRelation(rel),
getIdsValue("STATE.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x112: // SPELL STATE
if (isBitwiseRelation(rel) && value != -1) {
label = String.format("SPLSTATE %s %s [0x%x]",
getRelation(rel),
getIdsValue("SPLSTATE.IDS", value, isBitwiseRelation(rel)),
value);
} else {
label = String.format("SPLSTATE %s %s",
getRelation(rel),
getIdsValue("SPLSTATE.IDS", value, isBitwiseRelation(rel)));
}
break;
case 0x113: // source and target allies
switch (rel) {
case 1: label = "Allies"; break;
case 5: label = "Not allies"; break;
default: label = "Allies match: " + getRelation(rel); break;
}
break;
case 0x114: // source and target enemies
switch (rel) {
case 1: label = "Enemies"; break;
case 5: label = "Not enemies"; break;
default: label = "Enemies match: " + getRelation(rel); break;
}
break;
case 0x115: // summon creature limit
label = String.format("# summoned creatures %s specified value", getRelation(rel));
break;
case 0x116: // chapter check
label = String.format("Chapter %s specified value", getRelation(rel));
break;
default: // use values from STATS.IDS
if (stat >= 0 && stat < 0x100) {
// valid stat
if (value == -1) {
label = String.format("STAT %s %s specified value",
getIdsValue("STATS.IDS", stat, isBitwiseRelation(rel)),
getRelation(rel));
} else {
if (isBitwiseRelation(rel)) {
label = String.format("STAT %s %s %x",
getIdsValue("STATS.IDS", stat, isBitwiseRelation(rel)),
getRelation(rel), value);
} else {
label = String.format("STAT %s %s %d",
getIdsValue("STATS.IDS", stat, isBitwiseRelation(rel)),
getRelation(rel), value);
}
}
} else {
label = "Undefined";
}
break;
}
retVal[row] = label;
}
}
}
return retVal;
}
// Attempts to convert the specified string into a number. Returns the default value on error.
private static int toNumber(String value, int defValue)
{
int retVal = defValue;
if (value != null && !value.isEmpty()) {
try {
if (value.toLowerCase().startsWith("0x")) {
retVal = Integer.parseInt(value.substring(2), 16);
} else {
retVal = Integer.parseInt(value);
}
} catch (NumberFormatException e) {
}
}
return retVal;
}
// Returns the IDS symbol based on the specified arguments.
// Returns a decimal or hexadecimal number as string if symbol not found.
private static String getIdsValue(String idsFile, int value, boolean asHex)
{
if (value == -1) {
return "specified value";
} else {
IdsMap map = IdsMapCache.get(idsFile);
if (map != null) {
IdsMapEntry entry = map.getValue((long)value);
if (entry != null) {
if (entry.getString() != null && !entry.getString().isEmpty()) {
return entry.getString();
}
}
}
return asHex ? "0x" + Integer.toHexString(value) : Integer.toString(value);
}
}
// Returns a textual representation of the specified relation code
private static String getRelation(int rel)
{
return (rel >= 0 && rel < s_relation.length) ? s_relation[rel] : "";
}
// Returns whether specified code indicates a binary operator
private static boolean isBitwiseRelation(int rel)
{
return (rel >= 6 && rel <= 11);
}
// Creates a valid field name from the specified arguments
private static String createFieldName(String name, int index, String defName)
{
if (name == null) {
name = (defName != null) ? defName : DEFAULT_NAME_TYPE;
}
return (index >= 0) ? (name + " " + index) : name;
}
}