diff --git a/binaries/data/mods/public/simulation/templates/special_filter/construction.xml b/binaries/data/mods/public/simulation/templates/special_filter/construction.xml
new file mode 100644
index 0000000..c7f7da7
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special_filter/construction.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Entity filtered="">
+  <Footprint merge=""/>
+  <Ownership merge=""/>
+  <Position merge=""/>
+  <VisualActor merge=""/>
+</Entity>
diff --git a/binaries/data/mods/public/simulation/templates/special_filter/corpse.xml b/binaries/data/mods/public/simulation/templates/special_filter/corpse.xml
new file mode 100644
index 0000000..c1e20b5
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special_filter/corpse.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Entity filtered="">
+  <!--
+       We only want to include components which are necessary (for the visual previewing of an entity)
+       and safe (i.e. won't do anything that affects the synchronised simulation state), so additions
+       to this list should be carefully considered.
+       The Attack, Sound and UnitMotion components are needed for the Actor Viewer.
+  -->
+  <Attack merge=""/>
+  <BuildRestrictions merge=""/>
+  <!-- Corpses should include decay components and activate them -->
+  <Decay merge="">
+    <Active>true</Active>
+  </Decay>
+  <Footprint merge=""/>
+  <Identity merge=""/>
+  <!-- Disable the Obstruction component (if there is one) so it doesn't affect pathfinding
+       (but can still be used for testing this entity for collisions against others) -->
+  <Obstruction merge="">
+    <Active>false</Active>
+  </Obstruction>
+  <Ownership merge=""/>
+  <Position merge=""/>
+  <Sound merge=""/>
+  <UnitMotion merge=""/>
+  <!-- Corpses should remain visible in fog-of-war (for the owner only) -->
+  <Visibility>
+    <Corpse>true</Corpse>
+  </Visibility>
+  <!-- Corpses shouldn't display silhouettes (especially since they're often half underground) -->
+  <VisualActor merge="">
+    <SilhouetteDisplay>false</SilhouetteDisplay>
+  </VisualActor>
+</Entity>
diff --git a/binaries/data/mods/public/simulation/templates/special_filter/foundation.xml b/binaries/data/mods/public/simulation/templates/special_filter/foundation.xml
new file mode 100644
index 0000000..99b0e87
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special_filter/foundation.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Entity filtered="">
+  <AIProxy merge=""/>
+  <Armour merge=""/>
+  <BuildRestrictions merge=""/>
+  <!-- Don't provide population bonuses yet (but still do take up population cost) -->
+  <Cost merge="">
+    <PopulationBonus>0</PopulationBonus>
+  </Cost>
+  <Decay merge=""/>
+  <Fogging merge=""/>
+  <Footprint merge=""/>
+  <!-- Add the Foundation component, to deal with the construction process -->
+  <Foundation replace=""/>
+  <Health>
+    <Initial>1</Initial>
+  </Health>
+  <Identity merge=""/>
+  <Market merge=""/>
+  <!-- Foundations shouldn't initially block unit movement -->
+  <Obstruction merge="">
+    <DisableBlockMovement>true</DisableBlockMovement>
+    <DisableBlockPathfinding>true</DisableBlockPathfinding>
+  </Obstruction>
+  <OverlayRenderer merge=""/>
+  <Ownership merge=""/>
+  <Position merge=""/>
+  <RallyPoint merge=""/>
+  <RallyPointRenderer merge=""/>
+  <Selectable merge=""/>
+  <Sound merge=""/>
+  <StatusBars merge=""/>
+  <Visibility merge=""/>
+  <!-- Foundations should be visible themselves in fog-of-war if their base template is,
+       but shouldn't have any vision range -->
+  <Vision merge="">
+    <Range>0</Range>
+    <RevealShore>false</RevealShore>
+  </Vision>
+  <!-- Switch the actor to foundation mode -->
+  <VisualActor>
+    <Foundation/>
+  </VisualActor>
+</Entity>
diff --git a/binaries/data/mods/public/simulation/templates/special_filter/mirage.xml b/binaries/data/mods/public/simulation/templates/special_filter/mirage.xml
new file mode 100644
index 0000000..8f86013
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special_filter/mirage.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Entity filtered="">
+  <Footprint merge=""/>
+  <Identity filtered="">
+    <Civ merge=""/>
+    <GenericName merge=""/>
+    <SpecificName merge=""/>
+    <Tooltip merge=""/>
+    <History merge=""/>
+    <Icon merge=""/>
+  </Identity>
+  <Minimap merge=""/>
+  <Mirage replace=""/>
+  <Obstruction merge="">
+    <BlockMovement>false</BlockMovement>
+    <BlockPathfinding>false</BlockPathfinding>
+    <BlockFoundation>false</BlockFoundation>
+    <BlockConstruction>false</BlockConstruction>
+  </Obstruction>
+  <Ownership merge=""/>
+  <OverlayRenderer merge=""/>
+  <Position merge=""/>
+  <Selectable merge=""/>
+  <StatusBars merge=""/>
+  <Visibility merge=""/>
+  <VisualActor merge=""/>
+</Entity>
diff --git a/binaries/data/mods/public/simulation/templates/special_filter/preview.xml b/binaries/data/mods/public/simulation/templates/special_filter/preview.xml
new file mode 100644
index 0000000..0be3e4c
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special_filter/preview.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Entity filtered="">
+  <!--
+       We only want to include components which are necessary (for the visual previewing of an entity)
+       and safe (i.e. won't do anything that affects the synchronised simulation state), so additions
+       to this list should be carefully considered.
+       The Attack, Sound and UnitMotion components are needed for the Actor Viewer.
+  -->
+  <Attack merge=""/>
+  <BuildRestrictions merge=""/>
+  <Decay merge=""/>
+  <Footprint merge=""/>
+  <Identity merge=""/>
+  <!-- Disable the Obstruction component (if there is one) so it doesn't affect pathfinding
+       (but can still be used for testing this entity for collisions against others) -->
+  <Obstruction merge="">
+    <Active>false</Active>
+  </Obstruction>
+  <Ownership merge=""/>
+  <Position merge=""/>
+  <Sound merge=""/>
+  <UnitMotion merge=""/>
+  <!-- Previews should always be visible in fog-of-war/etc -->
+  <Visibility>
+    <AlwaysVisible>true</AlwaysVisible>
+    <Preview>true</Preview>
+  </Visibility>
+  <!-- Previews should not cast shadows -->
+  <VisualActor merge="">
+    <DisableShadows/>
+  </VisualActor>
+</Entity>
diff --git a/binaries/data/mods/public/simulation/templates/special_filter/resource.xml b/binaries/data/mods/public/simulation/templates/special_filter/resource.xml
new file mode 100644
index 0000000..ee62748
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special_filter/resource.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Entity filtered="">
+  <AIProxy merge=""/>
+  <Footprint merge=""/>
+  <Identity merge=""/>
+  <Minimap merge=""/>
+  <!-- When dying, resources lose the unitMotion component, this causes them to have no clearance.
+       Since unit obstructions no longer have a radius, this makes them unreachable in some cases (see #3530).
+       Instead, create a static, unblocking (see #3530 for why) static obstruction.
+       TODO: this should probably be generalized as a parameter on entity death or something.
+  -->
+  <Obstruction replace="">
+    <Active>true</Active>
+    <BlockMovement>false</BlockMovement>
+    <BlockPathfinding>false</BlockPathfinding>
+    <BlockFoundation>false</BlockFoundation>
+    <BlockConstruction>false</BlockConstruction>
+    <DisableBlockMovement>false</DisableBlockMovement>
+    <DisableBlockPathfinding>false</DisableBlockPathfinding>
+    <Static width="2.0" depth="2.0"/>
+  </Obstruction>
+  <OverlayRenderer merge=""/>
+  <Ownership merge=""/>
+  <Position merge=""/>
+  <ResourceSupply merge=""/>
+  <Selectable merge=""/>
+  <StatusBars merge=""/>
+  <VisualActor merge=""/>
+</Entity>
diff --git a/source/ps/TemplateLoader.cpp b/source/ps/TemplateLoader.cpp
index b9f7606..38edbe1 100644
--- a/source/ps/TemplateLoader.cpp
+++ b/source/ps/TemplateLoader.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* Copyright (C) 2016 Wildfire Games.
  * This file is part of 0 A.D.
  *
  * 0 A.D. is free software: you can redistribute it and/or modify
@@ -29,7 +29,6 @@ static const wchar_t ACTOR_ROOT[] = L"art/actors/";
 
 static CParamNode NULL_NODE(false);
 
-
 bool CTemplateLoader::LoadTemplateFile(const std::string& templateName, int depth)
 {
 	// If this file was already loaded, we don't need to do anything
@@ -50,93 +49,33 @@ bool CTemplateLoader::LoadTemplateFile(const std::string& templateName, int dept
 		return true;
 	}
 
-	// Handle special case "preview|foo"
-	if (templateName.find("preview|") == 0)
+	// Handle special case "bar|foo"
+	size_t pos = templateName.find_first_of('|');
+	if (pos != std::string::npos)
 	{
-		// Load the base entity template, if it wasn't already loaded
-		std::string baseName = templateName.substr(8);
-		if (!LoadTemplateFile(baseName, depth+1))
-		{
-			LOGERROR("Failed to load entity template '%s'", baseName.c_str());
-			return false;
-		}
-		// Copy a subset to the requested template
-		CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], false);
-		return true;
-	}
+		std::string prefix = templateName.substr(0, pos);
+		std::string baseName = templateName.substr(pos+1);
 
-	// Handle special case "corpse|foo"
-	if (templateName.find("corpse|") == 0)
-	{
-		// Load the base entity template, if it wasn't already loaded
-		std::string baseName = templateName.substr(7);
 		if (!LoadTemplateFile(baseName, depth+1))
 		{
 			LOGERROR("Failed to load entity template '%s'", baseName.c_str());
 			return false;
 		}
-		// Copy a subset to the requested template
-		CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], true);
-		return true;
-	}
 
-	// Handle special case "mirage|foo"
-	if (templateName.find("mirage|") == 0)
-	{
-		// Load the base entity template, if it wasn't already loaded
-		std::string baseName = templateName.substr(7);
-		if (!LoadTemplateFile(baseName, depth+1))
+		VfsPath path = VfsPath(TEMPLATE_ROOT) / L"special_filter" / wstring_from_utf8(prefix + ".xml");
+		if (!VfsFileExists(path))
 		{
-			LOGERROR("Failed to load entity template '%s'", baseName.c_str());
+			LOGERROR("Invalid subset '%s'", prefix.c_str());
 			return false;
 		}
-		// Copy a subset to the requested template
-		CopyMirageSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
-		return true;
-	}
 
-	// Handle special case "foundation|foo"
-	if (templateName.find("foundation|") == 0)
-	{
-		// Load the base entity template, if it wasn't already loaded
-		std::string baseName = templateName.substr(11);
-		if (!LoadTemplateFile(baseName, depth+1))
-		{
-			LOGERROR("Failed to load entity template '%s'", baseName.c_str());
-			return false;
-		}
-		// Copy a subset to the requested template
-		CopyFoundationSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
-		return true;
-	}
+		CXeromyces xero;
+		PSRETURN ok = xero.Load(g_VFS, path);
+		if (ok != PSRETURN_OK)
+			return false; // (Xeromyces already logged an error with the full filename)
 
-	// Handle special case "construction|foo"
-	if (templateName.find("construction|") == 0)
-	{
-		// Load the base entity template, if it wasn't already loaded
-		std::string baseName = templateName.substr(13);
-		if (!LoadTemplateFile(baseName, depth+1))
-		{
-			LOGERROR("Failed to load entity template '%s'", baseName.c_str());
-			return false;
-		}
-		// Copy a subset to the requested template
-		CopyConstructionSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
-		return true;
-	}
-
-	// Handle special case "resource|foo"
-	if (templateName.find("resource|") == 0)
-	{
-		// Load the base entity template, if it wasn't already loaded
-		std::string baseName = templateName.substr(9);
-		if (!LoadTemplateFile(baseName, depth+1))
-		{
-			LOGERROR("Failed to load entity template '%s'", baseName.c_str());
-			return false;
-		}
-		// Copy a subset to the requested template
-		CopyResourceSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]);
+		m_TemplateFileData[templateName] = m_TemplateFileData[baseName];
+		CParamNode::LoadXML(m_TemplateFileData[templateName], xero, path.string().c_str());
 		return true;
 	}
 
@@ -215,14 +154,19 @@ bool CTemplateLoader::TemplateExists(const std::string& templateName) const
 
 std::vector<std::string> CTemplateLoader::FindPlaceableTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType, ScriptInterface& scriptInterface)
 {
+	if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES)
+	{
+		LOGERROR("Undefined template type (valid: all, simulation, actor)");
+		return std::vector<std::string>();
+	}
+
 	JSContext* cx = scriptInterface.GetContext();
 	JSAutoRequest rq(cx);
-	
+
 	std::vector<std::string> templates;
 	Status ok;
 	VfsPath templatePath;
 
-
 	if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES)
 	{
 		JS::RootedValue placeablesFilter(cx);
@@ -259,11 +203,11 @@ std::vector<std::string> CTemplateLoader::FindPlaceableTemplates(const std::stri
 				std::wstring fileFilter;
 				scriptInterface.GetProperty(val, "directory", directoryPath);
 				scriptInterface.GetProperty(val, "file", fileFilter);
-				
+
 				VfsPaths filenames;
 				if (vfs::GetPathnames(g_VFS, templatePath / (directoryPath + "/"), fileFilter.c_str(), filenames) != INFO::OK)
 					continue;
-				
+
 				for (const VfsPath& filename : filenames)
 				{
 					// Strip the .xml extension
@@ -273,9 +217,8 @@ std::vector<std::string> CTemplateLoader::FindPlaceableTemplates(const std::stri
 
 					templates.emplace_back(name.begin(), name.end());
 				}
-				
+
 			}
-			
 		}
 	}
 
@@ -289,9 +232,6 @@ std::vector<std::string> CTemplateLoader::FindPlaceableTemplates(const std::stri
 		WARN_IF_ERR(ok);
 	}
 
-	if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES)
-		LOGERROR("Undefined template type (valid: all, simulation, actor)");
-
 	return templates;
 }
 
@@ -299,30 +239,19 @@ std::vector<std::string> CTemplateLoader::FindTemplates(const std::string& path,
 {
 	std::vector<std::string> templates;
 
-	Status ok;
-	VfsPath templatePath;
-
-	if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES)
-	{
-		templatePath = VfsPath(TEMPLATE_ROOT) / path;
-		if (includeSubdirectories)
-			ok = vfs::ForEachFile(g_VFS, templatePath, AddToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE);
-		else
-			ok = vfs::ForEachFile(g_VFS, templatePath, AddToTemplates, (uintptr_t)&templates, L"*.xml");
-		WARN_IF_ERR(ok);
-	}
-	if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES)
+	if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES)
 	{
-		templatePath = VfsPath(ACTOR_ROOT) / path;
-		if (includeSubdirectories)
-			ok = vfs::ForEachFile(g_VFS, templatePath, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE);
-		else
-			ok = vfs::ForEachFile(g_VFS, templatePath, AddActorToTemplates, (uintptr_t)&templates, L"*.xml");
-		WARN_IF_ERR(ok);
+		LOGERROR("Undefined template type (valid: all, simulation, actor)");
+		return templates;
 	}
 
-	if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES)
-		LOGERROR("Undefined template type (valid: all, simulation, actor)");
+	size_t flags = includeSubdirectories ? vfs::DIR_RECURSIVE : 0;
+
+	if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES)
+		WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(TEMPLATE_ROOT) / path, AddToTemplates, (uintptr_t)&templates, L"*.xml", flags));
+
+	if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES)
+		WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(ACTOR_ROOT) / path, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", flags));
 
 	return templates;
 }
@@ -367,203 +296,3 @@ void CTemplateLoader::ConstructTemplateActor(const std::string& actorName, CPara
 
 	CParamNode::LoadXMLString(out, xml.c_str(), actorNameW.c_str());
 }
-
-void CTemplateLoader::CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse)
-{
-	// We only want to include components which are necessary (for the visual previewing of an entity)
-	// and safe (i.e. won't do anything that affects the synchronised simulation state), so additions
-	// to this list should be carefully considered
-	std::set<std::string> permittedComponentTypes;
-	permittedComponentTypes.insert("Identity");
-	permittedComponentTypes.insert("Ownership");
-	permittedComponentTypes.insert("Position");
-	permittedComponentTypes.insert("Visibility");
-	permittedComponentTypes.insert("VisualActor");
-	permittedComponentTypes.insert("Footprint");
-	permittedComponentTypes.insert("Obstruction");
-	permittedComponentTypes.insert("Decay");
-	permittedComponentTypes.insert("BuildRestrictions");
-
-	// Need these for the Actor Viewer:
-	permittedComponentTypes.insert("Attack");
-	permittedComponentTypes.insert("UnitMotion");
-	permittedComponentTypes.insert("Sound");
-
-	// (This set could be initialised once and reused, but it's not worth the effort)
-
-	CParamNode::LoadXMLString(out, "<Entity/>");
-	out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
-
-	// Disable the Obstruction component (if there is one) so it doesn't affect pathfinding
-	// (but can still be used for testing this entity for collisions against others)
-	if (out.GetChild("Entity").GetChild("Obstruction").IsOk())
-		CParamNode::LoadXMLString(out, "<Entity><Obstruction><Active>false</Active></Obstruction></Entity>");
-
-	if (!corpse)
-	{
-		// Previews should not cast shadows
-		if (out.GetChild("Entity").GetChild("VisualActor").IsOk())
-			CParamNode::LoadXMLString(out, "<Entity><VisualActor><DisableShadows/></VisualActor></Entity>");
-
-		// Previews should always be visible in fog-of-war/etc
-		CParamNode::LoadXMLString(out, "<Entity><Visibility><AlwaysVisible>true</AlwaysVisible><Preview>true</Preview></Visibility></Entity>");
-	}
-
-	if (corpse)
-	{
-		// Corpses should include decay components and activate them
-		if (out.GetChild("Entity").GetChild("Decay").IsOk())
-			CParamNode::LoadXMLString(out, "<Entity><Decay><Active>true</Active></Decay></Entity>");
-
-		// Corpses shouldn't display silhouettes (especially since they're often half underground)
-		if (out.GetChild("Entity").GetChild("VisualActor").IsOk())
-			CParamNode::LoadXMLString(out, "<Entity><VisualActor><SilhouetteDisplay>false</SilhouetteDisplay></VisualActor></Entity>");
-
-		// Corpses should remain visible in fog-of-war (for the owner only)
-		CParamNode::LoadXMLString(out, "<Entity><Visibility><Corpse>true</Corpse></Visibility></Entity>");
-	}
-}
-
-void CTemplateLoader::CopyMirageSubset(CParamNode& out, const CParamNode& in)
-{
-	// Currently used for mirage entities replacing real ones in fog-of-war
-
-	std::set<std::string> permittedComponentTypes;
-	permittedComponentTypes.insert("Footprint");
-	permittedComponentTypes.insert("Minimap");
-	permittedComponentTypes.insert("Obstruction");
-	permittedComponentTypes.insert("Ownership");
-	permittedComponentTypes.insert("OverlayRenderer");
-	permittedComponentTypes.insert("Position");
-	permittedComponentTypes.insert("Selectable");
-	permittedComponentTypes.insert("StatusBars");
-	permittedComponentTypes.insert("Visibility");
-	permittedComponentTypes.insert("VisualActor");
-
-	CParamNode::LoadXMLString(out, "<Entity/>");
-	out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
-
-	// Select a subset of identity data. We don't want to have, for example, a CC mirage
-	// that has also the CC class and then prevents construction of other CCs
-	std::set<std::string> identitySubset;
-	identitySubset.insert("Civ");
-	identitySubset.insert("GenericName");
-	identitySubset.insert("SpecificName");
-	identitySubset.insert("Tooltip");
-	identitySubset.insert("History");
-	identitySubset.insert("Icon");
-	CParamNode identity;
-	CParamNode::LoadXMLString(identity, "<Identity/>");
-	identity.CopyFilteredChildrenOfChild(in.GetChild("Entity"), "Identity", identitySubset);
-	CParamNode::LoadXMLString(out, ("<Entity>"+utf8_from_wstring(identity.ToXML())+"</Entity>").c_str());
-
-	// Mirages obstruction shouldn't block anything
-	if (out.GetChild("Entity").GetChild("Obstruction").IsOk())
-		CParamNode::LoadXMLString(out, "<Entity><Obstruction><BlockMovement>false</BlockMovement><BlockPathfinding>false</BlockPathfinding><BlockFoundation>false</BlockFoundation><BlockConstruction>false</BlockConstruction></Obstruction></Entity>");
-
-	// Set the entity as mirage entity
-	CParamNode::LoadXMLString(out, "<Entity><Mirage/></Entity>");
-}
-
-void CTemplateLoader::CopyFoundationSubset(CParamNode& out, const CParamNode& in)
-{
-	// TODO: this is all kind of yucky and hard-coded; it'd be nice to have a more generic
-	// extensible scriptable way to define these subsets
-
-	std::set<std::string> permittedComponentTypes;
-	permittedComponentTypes.insert("Ownership");
-	permittedComponentTypes.insert("Position");
-	permittedComponentTypes.insert("VisualActor");
-	permittedComponentTypes.insert("Identity");
-	permittedComponentTypes.insert("BuildRestrictions");
-	permittedComponentTypes.insert("Obstruction");
-	permittedComponentTypes.insert("Selectable");
-	permittedComponentTypes.insert("Footprint");
-	permittedComponentTypes.insert("Fogging");
-	permittedComponentTypes.insert("Armour");
-	permittedComponentTypes.insert("Health");
-	permittedComponentTypes.insert("Market");
-	permittedComponentTypes.insert("StatusBars");
-	permittedComponentTypes.insert("OverlayRenderer");
-	permittedComponentTypes.insert("Decay");
-	permittedComponentTypes.insert("Cost");
-	permittedComponentTypes.insert("Sound");
-	permittedComponentTypes.insert("Visibility");
-	permittedComponentTypes.insert("Vision");
-	permittedComponentTypes.insert("AIProxy");
-	permittedComponentTypes.insert("RallyPoint");
-	permittedComponentTypes.insert("RallyPointRenderer");
-
-	CParamNode::LoadXMLString(out, "<Entity/>");
-	out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
-
-	// Switch the actor to foundation mode
-	CParamNode::LoadXMLString(out, "<Entity><VisualActor><Foundation/></VisualActor></Entity>");
-
-	// Add the Foundation component, to deal with the construction process
-	CParamNode::LoadXMLString(out, "<Entity><Foundation/></Entity>");
-
-	// Initialise health to 1
-	CParamNode::LoadXMLString(out, "<Entity><Health><Initial>1</Initial></Health></Entity>");
-
-	// Foundations shouldn't initially block unit movement
-	if (out.GetChild("Entity").GetChild("Obstruction").IsOk())
-		CParamNode::LoadXMLString(out, "<Entity><Obstruction><DisableBlockMovement>true</DisableBlockMovement><DisableBlockPathfinding>true</DisableBlockPathfinding></Obstruction></Entity>");
-
-	// Don't provide population bonuses yet (but still do take up population cost)
-	if (out.GetChild("Entity").GetChild("Cost").IsOk())
-		CParamNode::LoadXMLString(out, "<Entity><Cost><PopulationBonus>0</PopulationBonus></Cost></Entity>");
-
-	// Foundations should be visible themselves in fog-of-war if their base template is,
-	// but shouldn't have any vision range
-	if (out.GetChild("Entity").GetChild("Vision").IsOk())
-	{
-		CParamNode::LoadXMLString(out, "<Entity><Vision><Range>0</Range></Vision></Entity>");
-		// Foundations should not have special vision capabilities either
-		if (out.GetChild("Entity").GetChild("Vision").GetChild("RevealShore").IsOk())
-			CParamNode::LoadXMLString(out, "<Entity><Vision><RevealShore>false</RevealShore></Vision></Entity>");
-	}
-}
-
-void CTemplateLoader::CopyConstructionSubset(CParamNode& out, const CParamNode& in)
-{
-	// Currently used for buildings rising during construction
-	// Mostly serves to filter out components like Vision, UnitAI, etc.
-	std::set<std::string> permittedComponentTypes;
-	permittedComponentTypes.insert("Footprint");
-	permittedComponentTypes.insert("Ownership");
-	permittedComponentTypes.insert("Position");
-	permittedComponentTypes.insert("VisualActor");
-
-	CParamNode::LoadXMLString(out, "<Entity/>");
-	out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
-}
-
-void CTemplateLoader::CopyResourceSubset(CParamNode& out, const CParamNode& in)
-{
-	// Currently used for animals which die and leave a gatherable corpse.
-	// Mostly serves to filter out components like Vision, UnitAI, etc.
-	// Don't emit sound as our samples only apply to living animals.
-	std::set<std::string> permittedComponentTypes;
-	permittedComponentTypes.insert("Ownership");
-	permittedComponentTypes.insert("Position");
-	permittedComponentTypes.insert("VisualActor");
-	permittedComponentTypes.insert("Identity");
-	permittedComponentTypes.insert("Minimap");
-	permittedComponentTypes.insert("ResourceSupply");
-	permittedComponentTypes.insert("Selectable");
-	permittedComponentTypes.insert("Footprint");
-	permittedComponentTypes.insert("StatusBars");
-	permittedComponentTypes.insert("OverlayRenderer");
-	permittedComponentTypes.insert("AIProxy");
-
-	CParamNode::LoadXMLString(out, "<Entity/>");
-	out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes);
-	
-	// When dying, resources lose the unitMotion component
-	// This causes them to have no clearance. Since unit obstructions no longer have a radius,
-	// this makes them unreachable in some cases (see #3530).
-	// Instead, create a static, unblocking (see #3530 for why) static obstruction.
-	// TODO: this should probably be generalized as a parameter on entity death or something.
-	CParamNode::LoadXMLString(out, "<Entity><Obstruction><Active>true</Active><BlockMovement>false</BlockMovement><BlockPathfinding>false</BlockPathfinding><BlockFoundation>false</BlockFoundation><BlockConstruction>false</BlockConstruction><DisableBlockMovement>false</DisableBlockMovement><DisableBlockPathfinding>false</DisableBlockPathfinding><Static width=\"2.0\" depth=\"2.0\"/></Obstruction></Entity>");
-}
diff --git a/source/ps/TemplateLoader.h b/source/ps/TemplateLoader.h
index c34d303..d2a6514 100644
--- a/source/ps/TemplateLoader.h
+++ b/source/ps/TemplateLoader.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* Copyright (C) 2016 Wildfire Games.
  * This file is part of 0 A.D.
  *
  * 0 A.D. is free software: you can redistribute it and/or modify
@@ -48,7 +48,7 @@ public:
 	CTemplateLoader()
 	{
 	}
-	
+
 	/**
 	 * Provides the file data for requested template.
 	 */
@@ -81,36 +81,6 @@ private:
 	 */
 	void ConstructTemplateActor(const std::string& actorName, CParamNode& out);
 
-	/** 
-	 * Copy the non-interactive components of an entity template (position, actor, etc) into
-	 * a new entity template
-	 */
-	void CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse);
-
-	/** 
-	 * Copy the components of an entity template necessary for a fogged "mirage"
-	 * entity (position, actor) into a new entity template
-	 */
-	void CopyMirageSubset(CParamNode& out, const CParamNode& in);
-
-	/** 
-	 * Copy the components of an entity template necessary for a construction foundation
-	 * (position, actor, armour, health, etc) into a new entity template
-	 */
-	void CopyFoundationSubset(CParamNode& out, const CParamNode& in);
-
-	/** 
-	 * Copy the components of an entity template necessary for a non-foundation construction entity
-	 * into a new entity template
-	 */
-	void CopyConstructionSubset(CParamNode& out, const CParamNode& in);
-
-	/** 
-	 * Copy the components of an entity template necessary for a gatherable resource
-	 * into a new entity template
-	 */
-	void CopyResourceSubset(CParamNode& out, const CParamNode& in);
-
 	/**
 	 * Map from template name (XML filename or special |-separated string) to the most recently
 	 * loaded non-broken template data. This includes files that will fail schema validation.
diff --git a/source/simulation2/components/tests/test_scripts.h b/source/simulation2/components/tests/test_scripts.h
index 5cf8c2a..29014b4 100644
--- a/source/simulation2/components/tests/test_scripts.h
+++ b/source/simulation2/components/tests/test_scripts.h
@@ -59,10 +59,7 @@ public:
 	void test_scripts()
 	{
 		if (!VfsFileExists(L"simulation/components/tests/setup.js"))
-		{
-			debug_printf("WARNING: Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n");
 			return;
-		}
 
 		VfsPaths paths;
 		TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths));
diff --git a/source/simulation2/system/ParamNode.cpp b/source/simulation2/system/ParamNode.cpp
index 173117b..89ba11d 100644
--- a/source/simulation2/system/ParamNode.cpp
+++ b/source/simulation2/system/ParamNode.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* Copyright (C) 2016 Wildfire Games.
  * This file is part of 0 A.D.
  *
  * 0 A.D. is free software: you can redistribute it and/or modify
@@ -76,6 +76,8 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const
 	// Look for special attributes
 	int at_disable = xmb.GetAttributeID("disable");
 	int at_replace = xmb.GetAttributeID("replace");
+	int at_filtered = xmb.GetAttributeID("filtered");
+	int at_merge = xmb.GetAttributeID("merge");
 	int at_op = xmb.GetAttributeID("op");
 	int at_datatype = xmb.GetAttributeID("datatype");
 	enum op {
@@ -84,6 +86,8 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const
 		MUL
 	} op = INVALID;
 	bool replacing = false;
+	bool filtering = false;
+	bool merging = false;
 	{
 		XERO_ITER_ATTR(element, attr)
 		{
@@ -97,6 +101,16 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const
 				m_Childs.erase(name);
 				replacing = true;
 			}
+			else if (attr.Name == at_filtered)
+			{
+				filtering = true;
+			}
+			else if (attr.Name == at_merge)
+			{
+				if (m_Childs.find(name) == m_Childs.end())
+					return;
+				merging = true;
+			}
 			else if (attr.Name == at_op)
 			{
 				if (std::wstring(attr.Value.begin(), attr.Value.end()) == L"add") 
@@ -168,20 +182,32 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const
 		} 
 		hasSetValue = true; 
 	} 
-	if (!hasSetValue)
+	if (!hasSetValue && !merging)
 		node.m_Value = value;
 
+	// For the filtered case
+	ChildrenMap childs;
+
 	// Recurse through the element's children
 	XERO_ITER_EL(element, child)
 	{
 		node.ApplyLayer(xmb, child, sourceIdentifier);
+		if (filtering)
+		{
+			std::string childname = xmb.GetElementString(child.GetNodeName());
+			if (node.m_Childs.find(childname) != node.m_Childs.end())
+				childs[childname] = std::move(node.m_Childs[childname]);
+		}
 	}
 
+	if (filtering)
+		node.m_Childs.swap(childs);
+
 	// Add the element's attributes, prefixing names with "@"
 	XERO_ITER_ATTR(element, attr)
 	{
 		// Skip special attributes
-		if (attr.Name == at_replace || attr.Name == at_op)
+		if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered)
 			continue;
 		// Add any others
 		std::string attrName = xmb.GetAttributeString(attr.Name);
@@ -189,21 +215,6 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const
 	}
 }
 
-void CParamNode::CopyFilteredChildrenOfChild(const CParamNode& src, const char* name, const std::set<std::string>& permitted)
-{
-	ResetScriptVal();
-
-	ChildrenMap::iterator dstChild = m_Childs.find(name);
-	ChildrenMap::const_iterator srcChild = src.m_Childs.find(name);
-	if (dstChild == m_Childs.end() || srcChild == src.m_Childs.end())
-		return; // error
-
-	ChildrenMap::const_iterator it = srcChild->second.m_Childs.begin();
-	for (; it != srcChild->second.m_Childs.end(); ++it)
-		if (permitted.count(it->first))
-			dstChild->second.m_Childs[it->first] = it->second;
-}
-
 const CParamNode& CParamNode::GetChild(const char* name) const
 {
 	ChildrenMap::const_iterator it = m_Childs.find(name);
diff --git a/source/simulation2/system/ParamNode.h b/source/simulation2/system/ParamNode.h
index 4fc911f..0b18e5e 100644
--- a/source/simulation2/system/ParamNode.h
+++ b/source/simulation2/system/ParamNode.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* Copyright (C) 2016 Wildfire Games.
  * This file is part of 0 A.D.
  *
  * 0 A.D. is free software: you can redistribute it and/or modify
@@ -58,6 +58,15 @@ class XMBElement;
  *   <Example4 datatype="tokens">
  *     one two three
  *   </Example4>
+ *   <Example5>
+ *     <E/>
+ *     <F>
+ *       <I>test</I>
+ *     </F>
+ *     <H>
+ *       <J>example</J>
+ *     </H>
+ *   </Example5>
  * </Entity>
  * @endcode
  * then a second like:
@@ -75,6 +84,15 @@ class XMBElement;
  *     four             <!-- add a token to the parent's set -->
  *     -two             <!-- remove a token from the parent's set -->
  *   </Example4>
+ *   <Example5 filtered=""> <!-- drop all children of this node that are not in this file -->
+ *     <F merge="">  <!-- only add this element if it is also present in the parent -->
+ *       <K>example</K> <!-- if F is present merge its children normally -->
+ *     </F>
+ *     <G merge=""/>  <!-- keep the G element of the parent if it exists -->
+ *     <H>
+ *       <J>text</J>
+ *     </H>
+ *   </Example5>
  * </Entity>
  * @endcode
  * is equivalent to loading a single file like:
@@ -90,6 +108,15 @@ class XMBElement;
  *   <Example4>
  *     one three four
  *   </Example4>
+ *   <Example5>
+ *     <F>
+ *       <I>test</I>
+ *       <K>example</K>
+ *     </F>
+ *     <H>
+ *       <J>text</J>
+ *     </H>
+ *   </Example5>
  * </Entity>
  * @endcode
  *
@@ -103,7 +130,16 @@ class XMBElement;
  *     "Example3": {
  *       "D": "new"
  *     },
- *     "Example4": { "@datatype": "tokens", "_string": "one three four" }
+ *     "Example4": { "@datatype": "tokens", "_string": "one three four" },
+ *     "Example5": {
+ *       "F": {
+ *         "I": "test",
+ *         "K": "example"
+ *       },
+ *       "H": {
+ *         "J": "text"
+ *       }
+ *     }
  *   }
  * }
  * @endcode
@@ -146,14 +182,6 @@ public:
 	static PSRETURN LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier = NULL);
 
 	/**
-	 * Finds the childs named @a name from @a src and from @a this, and copies the source child's children
-	 * which are in the @a permitted set into this node's child.
-	 * Intended for use as a filtered clone of XML files.
-	 * @a this and @a src must have childs named @a name.
-	 */
-	void CopyFilteredChildrenOfChild(const CParamNode& src, const char* name, const std::set<std::string>& permitted);
-
-	/**
 	 * Returns the (unique) child node with the given name, or a node with IsOk() == false if there is none.
 	 */
 	const CParamNode& GetChild(const char* name) const;
diff --git a/source/simulation2/tests/test_CmpTemplateManager.h b/source/simulation2/tests/test_CmpTemplateManager.h
index 8103254..0fdc180 100644
--- a/source/simulation2/tests/test_CmpTemplateManager.h
+++ b/source/simulation2/tests/test_CmpTemplateManager.h
@@ -31,6 +31,8 @@
 #include "ps/CLogger.h"
 #include "ps/XML/Xeromyces.h"
 
+static const char SPECIAL_FILTER[] = "simulation/templates/special_filter/";
+
 class TestCmpTemplateManager : public CxxTest::TestSuite
 {
 public:
@@ -39,6 +41,10 @@ public:
 		g_VFS = CreateVfs(20 * MiB);
 		TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST));
 		TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache"));
+
+		// If the public mod exists, test the special_filter templates too
+		g_VFS->Mount(SPECIAL_FILTER, DataDir() / "mods" / "public" / SPECIAL_FILTER);
+
 		CXeromyces::Startup();
 	}
 
@@ -117,9 +123,23 @@ public:
 		ScriptInterface::ToJSVal(cx, &val, &actor->GetChild("VisualActor"));
 		TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})");
 
-		const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1", -1);
-		ScriptInterface::ToJSVal(cx, &val, &foundation->GetChild("VisualActor"));
-		TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), Foundation:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})");
+		if (VfsDirectoryExists(SPECIAL_FILTER))
+		{
+			const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1", -1);
+			ScriptInterface::ToJSVal(cx, &val, &foundation->GetChild("VisualActor"));
+
+			TS_ASSERT_STR_EQUALS(
+				man.GetScriptInterface().ToString(&val),
+				"({"
+					"Actor:\"example1\", "
+					"ActorOnly:(void 0), "
+					"Foundation:(void 0), "
+					"SilhouetteDisplay:\"false\", "
+					"SilhouetteOccluder:\"false\", "
+					"VisibleInAtlasOnly:\"false\""
+				"})"
+			);
+		}
 	}
 
 	void test_LoadTemplate_errors()
diff --git a/source/simulation2/tests/test_ParamNode.h b/source/simulation2/tests/test_ParamNode.h
index 043554b..83e11b9 100644
--- a/source/simulation2/tests/test_ParamNode.h
+++ b/source/simulation2/tests/test_ParamNode.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2016 Wildfire Games.
  * This file is part of 0 A.D.
  *
  * 0 A.D. is free software: you can redistribute it and/or modify
@@ -135,6 +135,164 @@ public:
 		TS_ASSERT_WSTR_EQUALS(node.ToXML(), L"<test><a datatype=\"tokens\">Y X</a></test>");
 	}
 
+	void test_overlay_filtered()
+	{
+		CParamNode node;
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test>"
+				"<a>"
+					"<b/>"
+				"</a>"
+				"<c>toberemoved</c>"
+				"<d>"
+					"<e/>"
+				"</d>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test filtered=\"\">"
+				"<a/>"
+				"<d>"
+					"<f/>"
+				"</d>"
+				"<g/>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_WSTR_EQUALS(node.ToXML(),
+			L"<test>"
+				"<a>"
+					"<b></b>"
+				"</a>"
+				"<d>"
+					"<e></e>"
+					"<f></f>"
+				"</d>"
+				"<g></g>"
+			"</test>");
+	}
+
+	void test_overlay_filtered_and_merge()
+	{
+		CParamNode node;
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test>"
+				"<a>"
+					"<b>b</b>"
+					"<c>c</c>"
+					"<d>d</d>"
+					"<e>e</e>"
+				"</a>"
+				"<f/>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test filtered=\"\">"
+				"<a filtered=\"\">"
+					"<b merge=\"\"/>"
+					"<c>c2</c>"
+					"<d/>"
+				"</a>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_WSTR_EQUALS(node.ToXML(),
+			L"<test>"
+				"<a>"
+					"<b>b</b>"
+					"<c>c2</c>"
+					"<d></d>"
+				"</a>"
+			"</test>");
+	}
+
+	void test_overlay_merge()
+	{
+		CParamNode node;
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test>"
+				"<a>"
+					"<b>foo</b>"
+					"<c>bar</c>"
+				"</a>"
+				"<x>"
+					"<y>"
+						"<z>foo</z>"
+					"</y>"
+				"</x>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test>"
+				"<a merge=\"\">"
+					"<b>test</b>"
+					"<d>baz</d>"
+				"</a>"
+				"<i merge=\"\">"
+					"<j>willnotbeincluded</j>"
+				"</i>"
+				"<x merge=\"\">"
+					"<y merge=\"\">"
+						"<v>text</v>"
+					"</y>"
+					"<w>more text</w>"
+				"</x>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_WSTR_EQUALS(node.ToXML(),
+			L"<test>"
+				"<a>"
+					"<b>test</b>"
+					"<c>bar</c>"
+					"<d>baz</d>"
+				"</a>"
+				"<x>"
+					"<w>more text</w>"
+					"<y>"
+						"<v>text</v>"
+						"<z>foo</z>"
+					"</y>"
+				"</x>"
+			"</test>");
+	}
+
+	void test_overlay_filtered_merge()
+	{
+		CParamNode node;
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test>"
+				"<a>"
+					"<b/>"
+				"</a>"
+				"<c>"
+					"<x/>"
+				"</c>"
+				"<Health>"
+					"<Max>1200</Max>"
+				"</Health>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node,
+			"<test filtered=\"\">"
+				"<c merge=\"\"/>"
+				"<d>bar</d>"
+				"<e merge=\"\"/>"
+				"<Health>"
+					"<Initial>1</Initial>"
+				"</Health>"
+			"</test>"), PSRETURN_OK);
+
+		TS_ASSERT_WSTR_EQUALS(node.ToXML(),
+				L"<test>"
+				"<Health>"
+					"<Initial>1</Initial>"
+					"<Max>1200</Max>"
+				"</Health>"
+				"<c>"
+					"<x></x>"
+				"</c>"
+				"<d>bar</d>"
+			"</test>");
+	}
+
 	void test_types()
 	{
 		CParamNode node;
