// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.bcs; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.infinity.NearInfinity; import org.infinity.gui.BrowserMenuBar; import org.infinity.gui.StatusBar; import org.infinity.resource.Profile; import org.infinity.resource.ResourceFactory; import org.infinity.resource.are.AreResource; import org.infinity.resource.cre.CreResource; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.IdsMap; import org.infinity.util.IdsMapCache; import org.infinity.util.IdsMapEntry; import org.infinity.util.Misc; import org.infinity.util.io.StreamUtils; public final class Compiler { /** Indicates how to compile the script source. */ public enum ScriptType { /** Treat source as full BAF resource. */ BAF, /** Treat source as script trigger only. */ TRIGGER, /** Treat source as script action only. */ ACTION, /** Do not compile automatically. */ CUSTOM } // Definition of triggers that don't use combined string and namespace arguments and supported engines (null = all engines) private static final HashMap<Long, Set<Profile.Engine>> separateNsTriggers = new HashMap<Long, Set<Profile.Engine>>(); static { separateNsTriggers.put(Long.valueOf(0x403F), null); // GlobalTimerExact(S:Name*,S:Area*) separateNsTriggers.put(Long.valueOf(0x4040), null); // GlobalTimerExpired(S:Name*,S:Area*) separateNsTriggers.put(Long.valueOf(0x4041), null); // GlobalTimerNotExpired(S:Name*,S:Area*) separateNsTriggers.put(Long.valueOf(0x4098), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // GlobalsEqual(S:Name1*,S:Name2*) separateNsTriggers.put(Long.valueOf(0x4099), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // GlobalsGT(S:Name1*,S:Name2*) separateNsTriggers.put(Long.valueOf(0x409A), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // GlobalsLT(S:Name1*,S:Name2*) separateNsTriggers.put(Long.valueOf(0x409B), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // LocalsEqual(S:Name1*,S:Name2*) separateNsTriggers.put(Long.valueOf(0x409C), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // LocalsGT(S:Name1*,S:Name2*) separateNsTriggers.put(Long.valueOf(0x409D), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // LocalsLT(S:Name1*,S:Name2*) separateNsTriggers.put(Long.valueOf(0x40B5), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // RealGlobalTimerExact(S:Name*,S:Area*) separateNsTriggers.put(Long.valueOf(0x40B6), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // RealGlobalTimerExpired(S:Name*,S:Area*) separateNsTriggers.put(Long.valueOf(0x40B7), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.BG2, Profile.Engine.EE))); // RealGlobalTimerNotExpired(S:Name*,S:Area*) separateNsTriggers.put(Long.valueOf(0x40E5), new HashSet<Profile.Engine>(Arrays.asList(Profile.Engine.EE))); // Switch(S:Global*,S:Area*) } private static final Map<String, Set<ResourceEntry>> scriptNamesCre = new HashMap<String, Set<ResourceEntry>>(); private static final Set<String> scriptNamesAre = new HashSet<String>(); private static boolean scriptNamesValid = false; private final SortedMap<Integer, String> errors = new TreeMap<Integer, String>(); private final SortedMap<Integer, String> warnings = new TreeMap<Integer, String>(); private IdsMap[] itype; private String emptyObject; private String source; // script source private String code; // compiled byte code private ScriptType scriptType; private int linenr; /** Globally initialize and cache creature and area references. */ public static synchronized void restartCompiler() { if (BrowserMenuBar.getInstance().checkScriptNames()) { scriptNamesCre.clear(); scriptNamesAre.clear(); scriptNamesValid = false; setupScriptNames(); } } // Returns whether the namespace argument is stored separately from the first string argument static boolean useSeparateNamespaceArgument(long id) { Long key = Long.valueOf(id); if (separateNsTriggers.containsKey(key)) { Set<Profile.Engine> set = separateNsTriggers.get(key); return (set == null || set.contains(Profile.getEngine())); } return false; } static boolean isPossibleNamespace(String string) { // TODO: simplify namespace detection if (string.equalsIgnoreCase("\"GLOBAL\"") || string.equalsIgnoreCase("\"LOCALS\"") || string.equalsIgnoreCase("\"MYAREA\"") || string.equalsIgnoreCase("\"KAPUTZ\"") || // PS:T string.length() == 8 && (string.toUpperCase(Locale.US).matches("\"\\S{2}\\d{4}\"") || ResourceFactory.resourceExists(unquoteString(string) + ".ARE"))) { return true; } return false; } public Compiler() { this("", ScriptType.BAF); } public Compiler(ResourceEntry bafEntry) throws Exception { this(bafEntry, ScriptType.BAF); } public Compiler(ResourceEntry bafEntry, ScriptType type) throws Exception { if (bafEntry == null) { throw new NullPointerException(); } init(); this.scriptType = type; setSource(bafEntry); } public Compiler(String source) { this(source, ScriptType.BAF); } public Compiler(String source, ScriptType type) { init(); this.scriptType = type; setSource(source); } /** Returns BAF script source. */ public String getSource() { return source; } /** Set new BAF script source. */ public void setSource(String source) { this.source = (source != null) ? source : ""; reset(); } /** Load new script source from the specified BAF resource entry. */ public void setSource(ResourceEntry bafEntry) throws Exception { if (bafEntry == null) { throw new NullPointerException(); } ByteBuffer buffer = bafEntry.getResourceBuffer(); this.source = StreamUtils.readString(buffer, buffer.limit()); reset(); } /** Returns compiled script byte code. */ public String getCode() { if (code == null) { compile(); } return code; } /** Returns currently used script type. */ public ScriptType getScriptType() { return scriptType; } /** * Specify new script type. * <b>Node:</b> Automatically invalidates previously compile script source. */ public void setScriptType(ScriptType type) { if (type != scriptType) { reset(); scriptType = type; } } public SortedMap<Integer, String> getErrors() { return errors; } public SortedMap<Integer, String> getWarnings() { return warnings; } public boolean hasValidScriptNames() { return scriptNamesValid; } public boolean hasScriptName(String scriptName) { if (scriptNamesValid && scriptNamesCre.containsKey(scriptName.toLowerCase(Locale.ENGLISH).replaceAll(" ", ""))) { return true; } return false; } public Set<ResourceEntry> getResForScriptName(String scriptName) { return scriptNamesCre.get(scriptName.toLowerCase(Locale.ENGLISH).replaceAll(" ", "")); } /** * Compiles the currently loaded script source into BCS byte code. * Uses {@link #getScriptType()} to determine the correct compile action. * @return The compiled BCS script byte code. */ public String compile() { switch (scriptType) { case BAF: return compileScript(); case TRIGGER: return compileTrigger(); case ACTION: return compileAction(); default: throw new IllegalArgumentException("Could not determine script type"); } } /** * Compiles the current script source as if defined as {@code ScriptType.BAF}. * @return The compiled BCS script byte code. Also available via {@link #getCode()}. */ public String compileScript() { reset(); StringBuilder sb = new StringBuilder("SC\n"); StringTokenizer st = new StringTokenizer(source, "\n", true); String line = null; if (st.hasMoreTokens()) line = getNextLine(st); while (st.hasMoreTokens()) { if (line == null || !line.equalsIgnoreCase("IF")) { String error = "Missing IF"; errors.put(new Integer(linenr), error); return "Error - " + error; } sb.append("CR\n"); compileCondition(sb, st); compileResponseSet(sb, st); sb.append("CR\n"); line = getNextLine(st); while (line.length() == 0 && st.hasMoreTokens()) line = getNextLine(st); } sb.append("SC\n"); code = sb.toString(); return code; } /** * Compiles the current script source as if defined as {@code ScriptType.Trigger}. * @return The compiled BCS script byte code. Also available via {@link #getCode()}. */ public String compileTrigger() { return compileDialog(false); } /** * Compiles the current script source as if defined as {@code ScriptType.Action}. * @return The compiled BCS script byte code. Also available via {@link #getCode()}. */ public String compileAction() { return compileDialog(true); } private void init() { if (Profile.getEngine() == Profile.Engine.PST) { itype = new IdsMap[]{ IdsMapCache.get("EA.IDS"), IdsMapCache.get("FACTION.IDS"), IdsMapCache.get("TEAM.IDS"), IdsMapCache.get("GENERAL.IDS"), IdsMapCache.get("RACE.IDS"), IdsMapCache.get("CLASS.IDS"), IdsMapCache.get("SPECIFIC.IDS"), IdsMapCache.get("GENDER.IDS"), IdsMapCache.get("ALIGN.IDS") }; } else if (Profile.getEngine() == Profile.Engine.IWD2) { itype = new IdsMap[]{ IdsMapCache.get("EA.IDS"), IdsMapCache.get("GENERAL.IDS"), IdsMapCache.get("RACE.IDS"), IdsMapCache.get("CLASS.IDS"), IdsMapCache.get("SPECIFIC.IDS"), IdsMapCache.get("GENDER.IDS"), IdsMapCache.get("ALIGNMNT.IDS"), IdsMapCache.get("SUBRACE.IDS"), IdsMapCache.get("CLASS.IDS"), IdsMapCache.get("CLASSMSK.IDS") }; } else { itype = new IdsMap[]{ IdsMapCache.get("EA.IDS"), IdsMapCache.get("GENERAL.IDS"), IdsMapCache.get("RACE.IDS"), IdsMapCache.get("CLASS.IDS"), IdsMapCache.get("SPECIFIC.IDS"), IdsMapCache.get("GENDER.IDS"), IdsMapCache.get("ALIGN.IDS") }; } emptyObject = compileObject(null, ""); } private static synchronized void setupScriptNames() { if (scriptNamesValid) { return; } Runnable worker = new Runnable() { @Override public void run() { StatusBar statusBar = NearInfinity.getInstance().getStatusBar(); String notification = "Gathering creature and area names ..."; String oldMessage = null; if (statusBar != null) { oldMessage = statusBar.getMessage(); statusBar.setMessage(notification); } ThreadPoolExecutor executor = Misc.createThreadPool(); List<ResourceEntry> files = ResourceFactory.getResources("CRE"); for (int i = 0; i < files.size(); i++) { Misc.isQueueReady(executor, true, -1); executor.execute(new CreWorker(files.get(i))); } files.clear(); files = ResourceFactory.getResources("ARE"); scriptNamesAre.add("none"); // default script name for many CRE resources for (int i = 0; i < files.size(); i++) { Misc.isQueueReady(executor, true, -1); executor.execute(new AreWorker(files.get(i))); } executor.shutdown(); try { executor.awaitTermination(60, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } if (statusBar != null) { if (statusBar.getMessage().startsWith(notification)) { statusBar.setMessage(oldMessage.trim()); } } scriptNamesValid = true; } }; new Thread(worker).start(); } private void reset() { linenr = 0; errors.clear(); warnings.clear(); code = null; } private String compileDialog(boolean isAction) { reset(); StringBuilder sb = new StringBuilder(); if (Profile.getEngine() == Profile.Engine.IWD || Profile.getEngine() == Profile.Engine.PST || Profile.getEngine() == Profile.Engine.IWD2) { StringTokenizer st = new StringTokenizer(source, ")"); while (st.hasMoreTokens()) { String line = st.nextToken().trim() + ')'; linenr++; int index = line.indexOf("//"); if (index != -1) { line = line.substring(0, index); } if (line.length() > 0 && !line.equals(")")) { if (isAction) { compileAction(sb, line); } else { compileTrigger(sb, line); } } } } else { StringTokenizer st = new StringTokenizer(source + "\n\n", "\n", true); String line = null; if (st.hasMoreTokens()) { line = getNextLine(st); } while (st.hasMoreTokens()) { if (isAction) { compileAction(sb, line); } else { compileTrigger(sb, line); } line = getNextLine(st); while (line.length() == 0 && st.hasMoreTokens()) { line = getNextLine(st); } } } code = sb.toString(); return code; } private void checkObjectString(String definition, String value) { String name = value.substring(1, value.length() - 1).toLowerCase(Locale.ENGLISH).replaceAll(" ", ""); if (scriptNamesValid) { if (name.equals("") || !(scriptNamesCre.containsKey(name) || scriptNamesAre.contains(name))) { warnings.put(new Integer(linenr), "Script name not found: " + definition + " - " + value); // } else { // System.out.println(definition + " - " + value + " OK"); } } } private void checkString(String function, String definition, String value) { String value_str = value.substring(1, value.length() - 1); String value_norm = value_str.toLowerCase(Locale.ENGLISH).replaceAll(" ", ""); // if (!definition.endsWith("*")) // System.out.println("Compiler.checkString: " + function + " " + definition + " " + value); if (value_str.isEmpty()) // ToDo: "" due to IWD2 decompiler bug? return; if (value_str.length() > 32) warnings.put(new Integer(linenr), "Invalid string length: " + definition + " - " + value); else if (definition.equalsIgnoreCase("S:Area*") || definition.equalsIgnoreCase("S:Area1*") || definition.equalsIgnoreCase("S:Area2*")) { if (!isPossibleNamespace(value)) { String error = "Invalid area string: " + definition + " - " + value; errors.put(new Integer(linenr), error); } } else if (definition.equalsIgnoreCase("S:Name*")) { // ToDo: need CalledByName()? if (scriptNamesValid) { if (function.equalsIgnoreCase("Dead(") || function.equalsIgnoreCase("IsScriptName(") || function.equalsIgnoreCase("Name(") || function.equalsIgnoreCase("NumDead(") || function.equalsIgnoreCase("NumDeadGT(") || function.equalsIgnoreCase("NumDeadLT(")) { if (!(scriptNamesCre.containsKey(value_norm) || scriptNamesAre.contains(value_norm)) && IdsMapCache.get("OBJECT.IDS").lookup(value) == null) warnings.put(new Integer(linenr), "Script name not found: " + definition + " - " + value); } else if (function.equalsIgnoreCase("SetCorpseEnabled(")) { if (!scriptNamesAre.contains(value_norm) && IdsMapCache.get("OBJECT.IDS").lookup(value) == null) warnings.put(new Integer(linenr), "Script name not found: " + definition + " - " + value); } } } else if (function.equalsIgnoreCase("AttachTransitionToDoor(") && scriptNamesValid) { if (!scriptNamesAre.contains(value_norm) && IdsMapCache.get("OBJECT.IDS").lookup(value) == null) warnings.put(new Integer(linenr), "Script name not found: " + definition + " - " + value); } // else if (definition.equalsIgnoreCase("S:Name*") || definition.equalsIgnoreCase("S:Column*") // || definition.equalsIgnoreCase("S:Entry*") || definition.equalsIgnoreCase("S:Global*") // || definition.equalsIgnoreCase("S:Name1*") || definition.equalsIgnoreCase("S:Name2*") // || definition.equalsIgnoreCase("S:Message*") || definition.equalsIgnoreCase("S:String1*") // || definition.equalsIgnoreCase("S:String2*") || definition.equalsIgnoreCase("S:String*") // || definition.equalsIgnoreCase("S:String3*") || definition.equalsIgnoreCase("S:String4*") // || definition.equalsIgnoreCase("S:VarTableEntry*") || definition.equalsIgnoreCase("S:Spells*") // || definition.equalsIgnoreCase("S:ScriptName*") || definition.equalsIgnoreCase("S:Sound1*")) { // // Not a resource // } else { // Resource checks String resourceTypes[] = new String[0]; if (definition.equalsIgnoreCase("S:DialogFile*")) resourceTypes = new String[] {".DLG", ".VEF", ".VVC", ".BAM"}; else if (definition.equalsIgnoreCase("S:CutScene*") || definition.equalsIgnoreCase("S:ScriptFile*") || definition.equalsIgnoreCase("S:Script*")) resourceTypes = new String[]{".BCS"}; else if (definition.equalsIgnoreCase("S:Item*") || definition.equalsIgnoreCase("S:Take*") || definition.equalsIgnoreCase("S:Give*") || definition.equalsIgnoreCase("S:Item") || definition.equalsIgnoreCase("S:OldObject*")) resourceTypes = new String[]{".ITM"}; else if (definition.equalsIgnoreCase("S:Sound*") || definition.equalsIgnoreCase("S:Voice*")) resourceTypes = new String[]{".WAV"}; else if (definition.equalsIgnoreCase("S:TextList*")) resourceTypes = new String[]{".2DA"}; else if (definition.equalsIgnoreCase("S:Effect*")) resourceTypes = new String[]{".VEF", ".VVC", ".BAM"}; else if (definition.equalsIgnoreCase("S:Parchment*")) resourceTypes = new String[]{".MOS"}; else if (definition.equalsIgnoreCase("S:Spell*") || definition.equalsIgnoreCase("S:Res*")) resourceTypes = new String[]{".SPL"}; else if (definition.equalsIgnoreCase("S:Store*")) resourceTypes = new String[]{".STO"}; else if (definition.equalsIgnoreCase("S:ToArea*") || definition.equalsIgnoreCase("S:Areaname*") || definition.equalsIgnoreCase("S:FromArea*")) resourceTypes = new String[]{".ARE"}; else if (definition.equalsIgnoreCase("S:BamResRef*")) resourceTypes = new String[]{".BAM"}; else if (definition.equalsIgnoreCase("S:Pool*")) resourceTypes = new String[]{".SRC"}; else if (definition.equalsIgnoreCase("S:Palette*")) resourceTypes = new String[]{".BMP"}; else if (definition.equalsIgnoreCase("S:ResRef*")) resourceTypes = Decompiler.getResRefType(function.substring(0, function.length() - 1)); else if (definition.equalsIgnoreCase("S:Object*")) resourceTypes = Decompiler.getResRefType(function.substring(0, function.length() - 1)); else if (definition.equalsIgnoreCase("S:NewObject*")) resourceTypes = Decompiler.getResRefType(function.substring(0, function.length() - 1)); if (resourceTypes.length > 0) { for (final String resourceType : resourceTypes) { if (ResourceFactory.resourceExists(value_str + resourceType, true)) { return; } } warnings.put(new Integer(linenr), "Resource not found: " + definition + " - " + value); } } // else // System.out.println(definition + " - " + value); } private void compileAction(StringBuilder code, String line) { int i = line.indexOf((int)'('); int j = line.lastIndexOf((int)')'); if (i == -1 || j == -1) { String error = "Missing parenthesis"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } String s_action = line.substring(0, i + 1); String s_param = line.substring(i + 1, j); while (s_action.endsWith(" (")) s_action = s_action.substring(0, s_action.length() - 2) + '('; IdsMapEntry idsEntry = IdsMapCache.get("ACTION.IDS").lookup(s_action); if (idsEntry == null) { String error = s_action + " not found in ACTION.IDS"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } String list_i[] = {"0", "0", "0"}; String list_o[] = {emptyObject, emptyObject, emptyObject}; String list_s[] = {null, null, null, null}; // Might be more than two because of ModifyStrings String list_p[] = {"0 0"}; int index_i = 0, index_o = 0, index_s = 0, index_p = 0; if (s_action.equalsIgnoreCase("ActionOverride(")) { list_o[index_o++] = compileObject("O:Actor*", s_param.substring(0, s_param.indexOf(',')).trim()); line = s_param.substring(s_param.indexOf(',') + 1).trim(); i = line.indexOf((int)'('); j = line.lastIndexOf((int)')'); if (i == -1 || j == -1) { String error = "Missing parenthesis"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } s_action = line.substring(0, i + 1); s_param = line.substring(i + 1, j).trim(); while (s_action.endsWith(" (")) s_action = s_action.substring(0, s_action.length() - 2) + '('; idsEntry = IdsMapCache.get("ACTION.IDS").lookup(s_action); if (idsEntry == null) { String error = s_action + " not found in ACTION.IDS"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } } else index_o++; code.append("AC\n").append(idsEntry.getID()); StringTokenizer actParam = new StringTokenizer(s_param, ","); StringTokenizer defParam = new StringTokenizer(idsEntry.getParameters(), ","); int defParamCount = defParam.countTokens(); while (actParam.hasMoreTokens()) { String parameter = actParam.nextToken().trim(); if (parameter.charAt(0) == '[' && parameter.charAt(parameter.length() - 1) != ']') { if (actParam.hasMoreTokens()) parameter += ',' + actParam.nextToken(); else { String error = "Missing end bracket - " + parameter; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); } } if (parameter.charAt(0) == '"' && parameter.charAt(parameter.length() - 1) != '"') { if (actParam.hasMoreTokens()) parameter += ',' + actParam.nextToken(); else { String error = "Missing end quote - " + parameter; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); } } if (!defParam.hasMoreTokens()) { String error = "Too many arguments - (" + idsEntry.getParameters() + ')'; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } String definition = defParam.nextToken(); if (definition.startsWith("I:") && actParam.hasMoreTokens() && !defParam.hasMoreTokens()) // Ugly fix - commas in IDS-files parameter = parameter + ',' + actParam.nextToken(); if (definition.startsWith("S:")) { if (index_s == 2 && parameter.charAt(0) != '"') list_o[index_o++] = compileObject(definition, parameter); else list_s[index_s++] = compileString(s_action, definition, parameter); } else if (definition.startsWith("I:")) list_i[index_i++] = compileInteger(definition, parameter); else if (definition.startsWith("O:")) list_o[index_o++] = compileObject(definition, parameter); else if (definition.startsWith("P:")) list_p[index_p++] = compilePoint(definition, parameter); } if (defParamCount > index_s + index_i + (index_o - 1) + index_p) { String error = "Too few arguments - (" + idsEntry.getParameters() + ')'; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } list_s = modifyStrings(list_s, (long)-1); code.append(list_o[0]).append('\n'); code.append(list_o[1]).append('\n'); code.append(list_o[2]).append('\n'); code.append(list_i[0]).append(' '); code.append(list_p[0]).append(' '); code.append(list_i[1]).append(' '); code.append(list_i[2]); code.append(list_s[0]).append(' '); code.append(list_s[1]).append(' '); code.append("AC\n"); } private void compileCondition(StringBuilder code, StringTokenizer st) { // IF last read token code.append("CO\n"); String line = getNextLine(st); int orCount = 0; while (!line.equalsIgnoreCase("THEN") && line.length() > 0) { int newOrCount = compileTrigger(code, line); if (newOrCount == 0 && orCount > 0) orCount--; else if (newOrCount > 0 && orCount > 0) { String error = "Nested ORs not allowed"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); } else orCount = newOrCount; line = getNextLine(st); } if (orCount > 0) { String error = "Missing " + orCount + " trigger(s) in order to match OR()"; errors.put(new Integer(linenr - 1), error); code.append("Error - ").append(error).append('\n'); } if (line.length() == 0) { String error = "Missing THEN"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); } code.append("CO\n"); } private String compileInteger(String definition, String value) { try { if (value.length() > 2 && value.substring(0, 2).equalsIgnoreCase("0x")) { int nr = Integer.parseInt(value.substring(2), 16); return Integer.toString(nr); } else { int nr = Integer.parseInt(value); return Integer.toString(nr); } } catch (NumberFormatException e) { } int i = definition.lastIndexOf((int)'*'); if (i == -1 || definition.substring(i + 1).length() == 0) { String error = "Expected " + definition + " but found " + value; errors.put(new Integer(linenr), error); return "Error - " + error; } IdsMap idsmap = IdsMapCache.get(definition.substring(i + 1).toUpperCase(Locale.ENGLISH) + ".IDS"); String code = idsmap.lookupID(value); if (code != null) return code; else if (value.indexOf("|") != -1) { long nr = (long)0; StringTokenizer st = new StringTokenizer(value, "|"); while (st.hasMoreTokens()) { String svalue = st.nextToken().trim(); IdsMapEntry idsentry = idsmap.lookup(svalue); if (idsentry == null) { String error = svalue + " not found in " + idsmap; errors.put(new Integer(linenr), error); return "Error - " + error; } nr += idsentry.getID(); } return Integer.toString((int)nr); } else { String error = value + " not found in " + idsmap; errors.put(new Integer(linenr), error); return "Error - " + error; } } private String compileObject(String definition, String value) { long identifiers[] = {0, 0, 0, 0, 0}; int firstIdentifier = -1; if (value.length() > 0 && value.charAt(0) != '"') { // Not straight string int i = value.indexOf((int)'('); while (i != -1) { if (value.charAt(value.length() - 1) != ')') { String error = "Missing end parenthesis " + value; errors.put(new Integer(linenr), error); return "Error - " + error; } IdsMapEntry idsEntry = IdsMapCache.get("OBJECT.IDS").lookup(value.substring(0, i)); if (idsEntry == null) { String error = value.substring(0, i) + " not found in OBJECT.IDS"; errors.put(new Integer(linenr), error); return "Error - " + error; } value = value.substring(i + 1, value.length() - 1); identifiers[++firstIdentifier] = idsEntry.getID(); i = value.indexOf((int)'('); } if (value.length() == 0 && firstIdentifier >= 0) value = "MYSELF"; } StringBuilder code = new StringBuilder("OB\n"); if (value.length() > 0 && value.charAt(0) == '[') { // Coordinate/ObjectType String coord = "[-1.-1.-1.-1]"; String iwd2 = " 0 0 "; while (value.charAt(0) == '[') { int endIndex = value.indexOf((int)']'); if (endIndex == -1) { String error = "Missing end bracket"; errors.put(new Integer(linenr), error); return "Error - " + error + '\n'; } String rest = value.substring(endIndex); if (endIndex == 1) // Enable [] shortcut value = "ANYONE"; else value = value.substring(1, endIndex); if (value.equalsIgnoreCase("ANYONE")) { if (itype.length == 7) code.append("0 0 0 0 0 0 0 "); else if (itype.length == 9) code.append("0 0 0 0 0 0 0 0 0 "); else if (itype.length == 10) code.append("0 0 0 0 0 0 0 0 "); } else { StringTokenizer st = new StringTokenizer(value, "."); boolean possiblecoord = true; StringBuilder temp = new StringBuilder(); for (final IdsMap idsMap : itype) { if (st.countTokens() > 4) possiblecoord = false; if (st.hasMoreTokens()) { String objType = st.nextToken(); IdsMapEntry idsEntry = idsMap.lookup(objType); if (idsEntry == null) { try { temp.append(Long.parseLong(objType)).append(' '); } catch (NumberFormatException e) { String error = objType + " not found in " + idsMap.toString().toUpperCase(Locale.ENGLISH); errors.put(new Integer(linenr), error); return "Error - " + error; } } else { temp.append(idsEntry.getID()).append(' '); possiblecoord = false; } } else temp.append("0 "); } if (possiblecoord && (Profile.getEngine() == Profile.Engine.PST || Profile.getEngine() == Profile.Engine.IWD || Profile.getEngine() == Profile.Engine.IWD2)) { if (code.toString().equals("OB\n")) { if (itype.length == 7) code.append("0 0 0 0 0 0 0 "); else if (itype.length == 9) code.append("0 0 0 0 0 0 0 0 0 "); else if (itype.length == 10) code.append("0 0 0 0 0 0 0 0 "); } coord = '[' + value + ']'; } else if (Profile.getEngine() == Profile.Engine.IWD2) { int space = temp.lastIndexOf(" "); space = temp.substring(0, space).lastIndexOf(" "); space = temp.substring(0, space).lastIndexOf(" "); code.append(temp.substring(0, space + 1)); iwd2 = temp.substring(space); } else code.append(temp); } int index = rest.indexOf((int)'['); if (index != -1) value = rest.substring(index); } for (int i = firstIdentifier; i >= 0; i--) { code.append(identifiers[i]).append(' '); } for (int i = firstIdentifier + 1; i < identifiers.length; i++) { code.append(identifiers[i]).append(' '); } if (Profile.getEngine() == Profile.Engine.PST || Profile.getEngine() == Profile.Engine.IWD) { code.append(coord).append(" \"\"OB"); } else if (Profile.getEngine() == Profile.Engine.IWD2) { code.append(coord).append(" \"\"").append(iwd2).append("OB"); } else { code.append("\"\"OB"); } } else if (value.length() > 0 && value.charAt(0) == '"') { // String if (value.charAt(value.length() - 1) != '"') { String error = "Missing end quote - " + value; errors.put(new Integer(linenr), error); return "Error - " + error; } if (itype.length == 7) { code.append("0 0 0 0 0 0 0 "); } else if (itype.length == 9) { code.append("0 0 0 0 0 0 0 0 0 "); } else if (itype.length == 10) { code.append("0 0 0 0 0 0 0 0 "); } for (int i = firstIdentifier; i >= 0; i--) { code.append(identifiers[i]).append(' '); } for (int i = firstIdentifier + 1; i < identifiers.length; i++) { code.append(identifiers[i]).append(' '); } if (Profile.getEngine() == Profile.Engine.PST || Profile.getEngine() == Profile.Engine.IWD) { code.append("[-1.-1.-1.-1] ").append(value).append("OB"); } else if (Profile.getEngine() == Profile.Engine.IWD2) { code.append("[-1.-1.-1.-1] ").append(value).append(" 0 0 OB"); } else { code.append(value).append("OB"); } checkObjectString(definition, value); } else { if (itype.length == 7) code.append("0 0 0 0 0 0 0 "); else if (itype.length == 9) code.append("0 0 0 0 0 0 0 0 0 "); else if (itype.length == 10) code.append("0 0 0 0 0 0 0 0 "); String coord; if (value.endsWith("]")) { coord = value.substring(value.indexOf((int)'[')); value = value.substring(0, value.indexOf((int)'[')); } else { coord = "[-1.-1.-1.-1]"; } IdsMapEntry idsEntry = IdsMapCache.get("OBJECT.IDS").lookup(value); if (idsEntry == null) { identifiers[++firstIdentifier] = (long)0; } else { identifiers[++firstIdentifier] = idsEntry.getID(); } if (coord.equals("[-1.-1.-1.-1]") && idsEntry == null && !value.equals("")) { String error = "Unknown symbol - " + value; errors.put(new Integer(linenr), error); return "Error - " + error; } for (int i = firstIdentifier; i >= 0; i--) { code.append(identifiers[i]).append(' '); } for (int i = firstIdentifier + 1; i < identifiers.length; i++) { code.append(identifiers[i]).append(' '); } if (Profile.getEngine() == Profile.Engine.PST || Profile.getEngine() == Profile.Engine.IWD) { code.append("[-1.-1.-1.-1] \"\"OB"); } else if (Profile.getEngine() == Profile.Engine.IWD2) { code.append(coord).append(" \"\" 0 0 OB"); } else { code.append("\"\"OB"); if (!coord.equals("[-1.-1.-1.-1]")) { String error = "Missing parenthesis?"; errors.put(new Integer(linenr), error); return "Error - " + error; } } } return code.toString(); } private String compilePoint(String definition, String value) { if (value.charAt(0) == '[' && value.charAt(value.length() - 1) == ']') { value = value.substring(1, value.length() - 1); // Remove '[' and ']' StringTokenizer st = new StringTokenizer(value, "."); StringBuilder code = new StringBuilder(); int countPeriod = 0; for (int i = 0; i < value.length(); i++) if (value.charAt(i) == '.') countPeriod++; if (countPeriod != st.countTokens() - 1) { String error = '[' + value + "] - arguments missing"; errors.put(new Integer(linenr), error); return "Error - " + error; } try { while (st.hasMoreTokens()) { String s = st.nextToken(); if (code.length() > 0) code.append(' '); code.append(Integer.parseInt(s)); } return code.toString(); } catch (NumberFormatException e) { String error = '[' + value + "] must contain numbers only"; errors.put(new Integer(linenr), error); return "Error - " + error; } } String error = "Expected " + definition + " but found " + value; errors.put(new Integer(linenr), error); return "Error - " + error; } private void compileResponseSet(StringBuilder code, StringTokenizer st) { // THEN last read token code.append("RS\n"); String line = getNextLine(st); boolean firstresponse = true; while (!line.equalsIgnoreCase("END") && line.length() > 0) { if (line.length() > 7 && line.substring(0, 8).equalsIgnoreCase("RESPONSE")) { if (!firstresponse) code.append("RE\n"); code.append("RE\n"); int i = line.indexOf((int)'#'); if (i == -1) { String error = "Missing # in RESPONSE"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return; } code.append(line.substring(i + 1)); firstresponse = false; } else compileAction(code, line); line = getNextLine(st); } if (line.length() == 0) { String error = "Missing END"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); } code.append("RE\nRS\n"); } private String compileString(String function, String definition, String value) { checkString(function, definition, value); return value; } private int compileTrigger(StringBuilder code, String line) // returns n if trigger = OR(n) { String flag; if (line.charAt(0) == '!') { flag = "1"; line = line.substring(1, line.length()); } else flag = "0"; int i = line.indexOf((int)'('); int j = line.lastIndexOf((int)')'); if (i == -1 || j == -1) { String error = "Missing parenthesis"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return 0; } String s_trigger = line.substring(0, i + 1); String s_param = line.substring(i + 1, j); while (s_trigger.endsWith(" (")) s_trigger = s_trigger.substring(0, s_trigger.length() - 2) + '('; IdsMapEntry idsEntry = IdsMapCache.get("TRIGGER.IDS").lookup(s_trigger); if (idsEntry == null) { String error = s_trigger + " not found in TRIGGER.IDS"; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return 0; } code.append("TR\n").append(idsEntry.getID()).append(' '); String integers[] = {"0", "0", "0"}; String object = null; String strings[] = {null, null, null, null}; String point = "[0,0]"; int indexI = 0, indexS = 0; StringTokenizer actParam = new StringTokenizer(s_param, ","); StringTokenizer defParam = new StringTokenizer(idsEntry.getParameters(), ","); int defParamCount = defParam.countTokens(), actParamCount = 0; while (actParam.hasMoreTokens()) { String parameter = actParam.nextToken().trim(); if (parameter.charAt(0) == '"' && parameter.charAt(parameter.length() - 1) != '"') { if (actParam.hasMoreTokens()) parameter += ',' + actParam.nextToken(); else { String error = "Missing end quote - " + parameter; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return 0; } } if (parameter.charAt(0) == '[' && parameter.charAt(parameter.length() - 1) != ']') { if (actParam.hasMoreTokens()) parameter += ',' + actParam.nextToken(); else { String error = "Missing end bracket - " + parameter; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return 0; } } if (!defParam.hasMoreTokens()) { String error = "Too many arguments - (" + idsEntry.getParameters() + ')'; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return 0; } String definition = defParam.nextToken(); if (definition.startsWith("I:") && actParam.hasMoreTokens() && !defParam.hasMoreTokens()) // Ugly fix - commas in IDS-files parameter = parameter + ',' + actParam.nextToken(); if (definition.startsWith("S:")) strings[indexS++] = compileString(s_trigger, definition, parameter); else if (definition.startsWith("I:")) integers[indexI++] = compileInteger(definition, parameter); else if (definition.startsWith("O:")) object = compileObject(definition, parameter); else if (definition.startsWith("P:")) point = parameter.replaceFirst("\\.", ","); // be consistent with WeiDU actParamCount++; } if (defParamCount > actParamCount) { String error = "Too few arguments - (" + idsEntry.getParameters() + ')'; errors.put(new Integer(linenr), error); code.append("Error - ").append(error).append('\n'); return 0; } if (object == null) object = emptyObject; strings = modifyStrings(strings, idsEntry.getID()); code.append(integers[0]).append(' '); code.append(flag).append(' '); code.append(integers[1]).append(' '); code.append(integers[2]).append(' '); if (Profile.getEngine() == Profile.Engine.PST) { code.append(point).append(' '); } code.append(strings[0]).append(' '); code.append(strings[1]).append(' '); code.append(object).append('\n'); code.append("TR\n"); if (s_trigger.equalsIgnoreCase("OR(")) return Integer.parseInt(integers[0]); return 0; } private String getNextLine(StringTokenizer st) { if (!st.hasMoreTokens()) return ""; String line = st.nextToken(); if (!line.equals("\n") && st.hasMoreTokens()) st.nextToken(); linenr++; int i = line.indexOf("//"); if (i != -1) line = line.substring(0, i); line = line.trim(); if (line.length() == 0) line = getNextLine(st); return line; } private String[] modifyStrings(String strings[], long id) { String newStrings[] = new String[strings.length]; int newIndex = 0; for (int i = 0; i < strings.length; i++) { String s = strings[i]; if (s != null) { boolean q_start = (s.length() > 0 && s.charAt(0) == '"'); boolean q_end = (s.length() > 1 && s.charAt(s.length() - 1) == '"'); if (!q_start && q_end) { errors.put(new Integer(linenr), "Missing begin quote - " + s); } else if (q_start && !q_end) { errors.put(new Integer(linenr), "Missing end quote - " + s); } else if (!q_start && !q_end) { errors.put(new Integer(linenr), "Missing quotes - " + s); } s = quoteString(s); if (useSeparateNamespaceArgument(id)) { newStrings[newIndex++] = s; } else if (newIndex > 0 && isPossibleNamespace(s) && !isPossibleNamespace(newStrings[newIndex - 1])) { newStrings[newIndex - 1] = quoteString(unquoteString(s) + unquoteString(newStrings[newIndex - 1])); } else if (newIndex > 1) { newStrings[newIndex - 1] = quoteString(unquoteString(newStrings[newIndex - 1]) + ":" + unquoteString(s)); } else { newStrings[newIndex++] = s; } } else { newStrings[i] = "\"\""; } } while (newIndex < newStrings.length) { newStrings[newIndex++] = "\"\""; } return newStrings; } private static String unquoteString(String s) { if (s != null) { int startIdx = (s.length() > 0 && s.charAt(0) == '"') ? 1 : 0; int endIdx = (s.length() > 1 && s.charAt(s.length() - 1) == '"') ? s.length() - 1 : s.length(); return s.substring(startIdx, endIdx); } else { return null; } } private static String quoteString(String s) { if (s != null) { StringBuilder sb = new StringBuilder(s.length() + 2); if (s.length() == 0 || s.charAt(0) != '"') { sb.append('"'); } sb.append(s); if (s.length() < 2 || s.charAt(s.length() - 1) != '"') { sb.append('"'); } return sb.toString(); } else { return null; } } //-------------------------- INNER CLASSES -------------------------- private static class CreWorker implements Runnable { final ResourceEntry entry; public CreWorker(ResourceEntry entry) { this.entry = entry; } @Override public void run() { if (entry != null) { try { CreResource.addScriptName(scriptNamesCre, entry); } catch (Exception e) { } } } } private static class AreWorker implements Runnable { final ResourceEntry entry; public AreWorker(ResourceEntry entry) { this.entry = entry; } @Override public void run() { if (entry != null) { try { AreResource.addScriptNames(scriptNamesAre, entry.getResourceBuffer()); } catch (Exception e) { } } } } }