diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index be5724c5d..ad0d9e245 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -377,11 +377,9 @@ simedtdlg.lbl.Timestep = Time step:
 simedtdlg.lbl.ttip.Timestep1 = The time between simulation steps.
A smaller time step results in a more accurate but slower simulation.
 simedtdlg.lbl.ttip.Timestep2 = The 4th order simulation method is quite accurate with a time step of
 simedtdlg.but.ttip.resettodefault = Reset the time step to its default value (
-simedtdlg.border.Simlist = Simulator listeners
-simedtdlg.txt.longA1 = Simulation listeners is an advanced feature that allows user-written code to listen to and interact with the simulation.
-simedtdlg.txt.longA2 = For details on writing simulation listeners, see the OpenRocket technical documentation.
-simedtdlg.lbl.Curlist = Current listeners:
-simedtdlg.lbl.Addsimlist = Add simulation listener
+simedtdlg.border.SimExt = Simulation extensions
+simedtdlg.SimExt.desc = Simulation extensions enable advanced features and custom functionality during flight simulations.  You can for example do hardware-in-the-loop testing with them.
+simedtdlg.SimExt.noExtensions = No simulation extensions defined
 simedtdlg.lbl.Noflightdata = No flight data available.
 simedtdlg.lbl.runsimfirst = Please run the simulation first.
 simedtdlg.chart.Simflight = Simulated flight
@@ -394,6 +392,12 @@ simedtdlg.IntensityDesc.High = High
 simedtdlg.IntensityDesc.Veryhigh = Very high
 simedtdlg.IntensityDesc.Extreme = Extreme
 
+SimulationExtension.airstart.name = Air-start ({alt})
+SimulationExtension.javacode.name = Java code
+SimulationExtension.javacode.name.none = none
+SimulationExtension.javacode.desc = Add a custom SimulationListener to the simulation
+SimulationExtension.javacode.className = Fully-qualified Java class name:
+
 SimulationEditDialog.btn.plot = Plot
 SimulationEditDialog.btn.export = Export
 SimulationEditDialog.btn.edit = Edit
diff --git a/core/resources/pix/icons/configure.png b/core/resources/pix/icons/configure.png
new file mode 100644
index 000000000..a4a3834ab
Binary files /dev/null and b/core/resources/pix/icons/configure.png differ
diff --git a/core/src/net/sf/openrocket/document/OpenRocketDocument.java b/core/src/net/sf/openrocket/document/OpenRocketDocument.java
index c479005b9..aec3d0333 100644
--- a/core/src/net/sf/openrocket/document/OpenRocketDocument.java
+++ b/core/src/net/sf/openrocket/document/OpenRocketDocument.java
@@ -23,7 +23,7 @@ import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.customexpression.CustomExpression;
-import net.sf.openrocket.simulation.listeners.SimulationListener;
+import net.sf.openrocket.simulation.extension.SimulationExtension;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.ArrayList;
 
@@ -146,17 +146,8 @@ public class OpenRocketDocument implements ComponentChangeListener {
 		
 		// simulation listeners
 		for (Simulation sim : simulations) {
-			for (String className : sim.getSimulationListeners()) {
-				SimulationListener l = null;
-				try {
-					Class> c = Class.forName(className);
-					l = (SimulationListener) c.newInstance();
-					
-					Collections.addAll(allTypes, l.getFlightDataTypes());
-					//System.out.println("This document has expression datatype from "+l.getName());
-				} catch (Exception e) {
-					log.error("Could not instantiate listener: " + className);
-				}
+			for (SimulationExtension c : sim.getSimulationExtensions()) {
+				allTypes.addAll(c.getFlightDataTypes());
 			}
 		}
 		
diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java
index 594734e2b..0b672032e 100644
--- a/core/src/net/sf/openrocket/document/Simulation.java
+++ b/core/src/net/sf/openrocket/document/Simulation.java
@@ -21,7 +21,7 @@ import net.sf.openrocket.simulation.SimulationEngine;
 import net.sf.openrocket.simulation.SimulationOptions;
 import net.sf.openrocket.simulation.SimulationStepper;
 import net.sf.openrocket.simulation.exception.SimulationException;
-import net.sf.openrocket.simulation.exception.SimulationListenerException;
+import net.sf.openrocket.simulation.extension.SimulationExtension;
 import net.sf.openrocket.simulation.listeners.SimulationListener;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.ArrayList;
@@ -76,7 +76,8 @@ public class Simulation implements ChangeSource, Cloneable {
 	// TODO: HIGH: Change to use actual conditions class??
 	private SimulationOptions options;
 	
-	private ArrayList simulationListeners = new ArrayList();
+	private ArrayList simulationExtensions = new ArrayList();
+	
 	
 	private final Class extends SimulationEngine> simulationEngineClass = BasicEventSimulationEngine.class;
 	private Class extends SimulationStepper> simulationStepperClass = RK4SimulationStepper.class;
@@ -116,7 +117,7 @@ public class Simulation implements ChangeSource, Cloneable {
 	
 	
 	public Simulation(Rocket rocket, Status status, String name, SimulationOptions options,
-			List listeners, FlightData data) {
+			List extensions, FlightData data) {
 		
 		if (rocket == null)
 			throw new IllegalArgumentException("rocket cannot be null");
@@ -142,8 +143,8 @@ public class Simulation implements ChangeSource, Cloneable {
 		this.options = options;
 		options.addChangeListener(new ConditionListener());
 		
-		if (listeners != null) {
-			this.simulationListeners.addAll(listeners);
+		if (extensions != null) {
+			this.simulationExtensions.addAll(extensions);
 		}
 		
 		
@@ -196,14 +197,14 @@ public class Simulation implements ChangeSource, Cloneable {
 	
 	
 	/**
-	 * Get the list of simulation listeners.  The returned list is the one used by
+	 * Get the list of simulation extensions.  The returned list is the one used by
 	 * this object; changes to it will reflect changes in the simulation.
 	 *
-	 * @return	the actual list of simulation listeners.
+	 * @return	the actual list of simulation extensions.
 	 */
-	public List getSimulationListeners() {
+	public List getSimulationExtensions() {
 		mutex.verify();
-		return simulationListeners;
+		return simulationExtensions;
 	}
 	
 	
@@ -293,16 +294,8 @@ public class Simulation implements ChangeSource, Cloneable {
 				simulationConditions.getSimulationListenerList().add(l);
 			}
 			
-			for (String className : simulationListeners) {
-				SimulationListener l = null;
-				try {
-					Class> c = Class.forName(className);
-					l = (SimulationListener) c.newInstance();
-				} catch (Exception e) {
-					throw new SimulationListenerException("Could not instantiate listener of " +
-							"class: " + className, e);
-				}
-				simulationConditions.getSimulationListenerList().add(l);
+			for (SimulationExtension extension : simulationExtensions) {
+				extension.initialize(simulationConditions);
 			}
 			
 			long t1, t2;
@@ -410,7 +403,9 @@ public class Simulation implements ChangeSource, Cloneable {
 			copy.mutex = SafetyMutex.newInstance();
 			copy.status = Status.NOT_SIMULATED;
 			copy.options = this.options.clone();
-			copy.simulationListeners = this.simulationListeners.clone();
+			for (SimulationExtension c : this.simulationExtensions) {
+				copy.simulationExtensions.add(c.clone());
+			}
 			copy.listeners = new ArrayList();
 			copy.simulatedConditions = null;
 			copy.simulatedConfiguration = null;
@@ -442,7 +437,9 @@ public class Simulation implements ChangeSource, Cloneable {
 			copy.name = this.name;
 			copy.options.copyFrom(this.options);
 			copy.simulatedConfiguration = this.simulatedConfiguration;
-			copy.simulationListeners = this.simulationListeners.clone();
+			for (SimulationExtension c : this.simulationExtensions) {
+				copy.simulationExtensions.add(c.clone());
+			}
 			copy.simulationStepperClass = this.simulationStepperClass;
 			copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass;
 			
diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java
index a3f085331..108c634fa 100644
--- a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java
+++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java
@@ -30,8 +30,10 @@ import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.FlightEvent;
 import net.sf.openrocket.simulation.SimulationOptions;
 import net.sf.openrocket.simulation.customexpression.CustomExpression;
+import net.sf.openrocket.simulation.extension.SimulationExtension;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.BuildProperties;
+import net.sf.openrocket.util.Config;
 import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.Reflection;
 import net.sf.openrocket.util.TextUtil;
@@ -455,9 +457,18 @@ public class OpenRocketSaver extends RocketSaver {
 		indent--;
 		writeln("");
 		
-		
-		for (String s : simulation.getSimulationListeners()) {
-			writeElement("listener", TextUtil.escapeXML(s));
+		for (SimulationExtension extension : simulation.getSimulationExtensions()) {
+			Config config = extension.getConfig();
+			writeln("");
+			indent++;
+			if (config != null) {
+				for (String key : config.keySet()) {
+					Object value = config.get(key, null);
+					writeEntry(key, value);
+				}
+			}
+			indent--;
+			writeln("");
 		}
 		
 		// Write basic simulation data
@@ -512,6 +523,39 @@ public class OpenRocketSaver extends RocketSaver {
 	}
 	
 	
+	private void writeEntry(String key, Object value) throws IOException {
+		if (value == null) {
+			return;
+		}
+		String keyAttr;
+		
+		if (key != null) {
+			keyAttr = "key=\"" + key + "\" ";
+		} else {
+			keyAttr = "";
+		}
+		
+		if (value instanceof Boolean) {
+			writeln("" + value + "");
+		} else if (value instanceof Number) {
+			writeln("" + value + "");
+		} else if (value instanceof String) {
+			writeln("" + value + "");
+		} else if (value instanceof List) {
+			List> list = (List>) value;
+			writeln("");
+			indent++;
+			for (Object o : list) {
+				writeEntry(null, o);
+			}
+			indent--;
+			writeln("");
+		} else {
+			// Unknown type
+			log.error("Unknown configuration value type " + value.getClass() + "  value=" + value);
+		}
+	}
+	
 	private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
 			throws IOException {
 		double previousTime = -100000;
diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java
new file mode 100644
index 000000000..3ecc6cc35
--- /dev/null
+++ b/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java
@@ -0,0 +1,104 @@
+package net.sf.openrocket.file.openrocket.importt;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.file.simplesax.AbstractElementHandler;
+import net.sf.openrocket.file.simplesax.ElementHandler;
+import net.sf.openrocket.file.simplesax.PlainTextHandler;
+import net.sf.openrocket.util.ArrayList;
+import net.sf.openrocket.util.Config;
+
+import org.xml.sax.SAXException;
+
+public class ConfigHandler extends AbstractElementHandler {
+	
+	private ConfigHandler listHandler;
+	private Config config = new Config();
+	private List