Index: binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml	(revision 16728)
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_archer_b.xml	(working copy)
@@ -6,9 +6,6 @@
       <animations>
         <animation file="biped/inf_sword_ready_a.dae" name="Idle" speed="100"/>
         <animation file="biped/inf_sword_ready_a.dae" name="Idle" speed="97"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_01.psa" name="attack_capture" speed="100"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_capture" speed="100"/>
-        <animation event="0.2" file="infantry/sword/attack/isw_s_em_04.psa" name="attack_capture" speed="100"/>
         <animation event="0.5" file="infantry/sword/attack/isw_s_off_05.psa" name="attack_slaughter" speed="100"/>
         <animation event="0.84" file="biped/inf_arch_atk_a.psa" load="0.16" name="attack_ranged" speed="90"/>
         <animation file="infantry/general/dude/dudewalk.psa" name="Walk" speed="100"/>
@@ -44,11 +41,7 @@
   </group>
   <group>
     <variant frequency="100" name="Idle"/>
-    <variant name="attack_capture">
-      <props>
-        <prop actor="props/units/tools/pitchfork.xml" attachpoint="r_hand"/>
-      </props>
-    </variant>
+    <variant file="units/variants/biped_attack_capture.xml"/>
     <variant name="attack_ranged">
       <props>
         <prop actor="props/units/weapons/bow_short.xml" attachpoint="l_hand"/>
Index: binaries/data/mods/public/art/actors/units/athenians/infantry_javelinist_b.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_javelinist_b.xml	(revision 16728)
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_javelinist_b.xml	(working copy)
@@ -5,9 +5,6 @@
     <variant frequency="100" name="Base">
       <animations>
         <animation file="infantry/general/dude/dudeidle.psa" name="Idle" speed="100"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_01.psa" name="attack_capture" speed="100"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_capture" speed="100"/>
-        <animation event="0.2" file="infantry/sword/attack/isw_s_em_04.psa" name="attack_capture" speed="100"/>
         <animation event="0.5" file="infantry/javelin/attack/ijv_off_01.psa" load="0" name="attack_ranged" speed="75"/>
         <animation event="0.5" file="infantry/sword/attack/isw_s_off_05.psa" name="attack_slaughter" speed="100"/>
         <animation file="biped/walk_spearshield.psa" name="Walk" speed="120"/>
@@ -48,11 +45,7 @@
   </group>
   <group>
     <variant frequency="100" name="Idle"/>
-    <variant name="attack_capture">
-      <props>
-        <prop actor="props/units/tools/pitchfork.xml" attachpoint="r_hand"/>
-      </props>
-    </variant>
+    <variant file="units/variants/biped_attack_capture.xml"/>
     <variant name="attack_ranged">
       <props>
         <prop attachpoint="r_hand"/>
Index: binaries/data/mods/public/art/actors/units/athenians/infantry_slinger_b.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_slinger_b.xml	(revision 16728)
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_slinger_b.xml	(working copy)
@@ -11,9 +11,6 @@
         <animation file="biped/walk_spearshield.psa" name="carry_wood" speed="120"/>
         <animation file="biped/walk_spearshield.psa" name="carry_stone" speed="120"/>
         <animation file="biped/walk_spearshield.psa" name="carry_metal" speed="120"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_01.psa" name="attack_capture" speed="100"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_capture" speed="100"/>
-        <animation event="0.2" file="infantry/sword/attack/isw_s_em_04.psa" name="attack_capture" speed="100"/>
 
         <animation event="0.5" file="biped/inf_sling_atk_a.psa" name="attack_ranged" speed="30"/>
         <animation event="0.5" file="infantry/sword/attack/isw_s_off_05.psa" name="attack_slaughter" speed="100"/>
@@ -46,11 +43,7 @@
   </group>
   <group>
     <variant frequency="1" name="Idle"/>
-    <variant name="attack_capture">
-      <props>
-        <prop actor="props/units/tools/pitchfork.xml" attachpoint="r_hand"/>
-      </props>
-    </variant>
+    <variant file="units/variants/biped_attack_capture.xml"/>
     <variant name="attack_slaughter">
       <props>
         <prop actor="props/units/weapons/knife.xml" attachpoint="r_hand"/>
Index: binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_b.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_b.xml	(revision 16728)
+++ binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_b.xml	(working copy)
@@ -12,9 +12,6 @@
         <animation file="biped/walk_spearshield.psa" name="carry_wood" speed="120"/>
         <animation file="biped/walk_spearshield.psa" name="carry_stone" speed="120"/>
         <animation file="biped/walk_spearshield.psa" name="carry_metal" speed="120"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_01.psa" name="attack_capture" speed="100"/>
-        <animation event="0.4" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_capture" speed="100"/>
-        <animation event="0.2" file="infantry/sword/attack/isw_s_em_04.psa" name="attack_capture" speed="100"/>
         <animation event="0.5" file="biped/inf_hoplite_atk_a.psa" name="attack_melee" speed="200"/>
         <animation event="0.5" file="biped/inf_hoplite_atk_a.psa" name="attack_slaughter" speed="200"/>
         <animation event="0.23" file="infantry/general/chop.psa" name="gather_tree" speed="250"/>
@@ -55,11 +52,7 @@
   </group>
   <group>
     <variant frequency="1" name="Idle"/>
-    <variant name="attack_capture">
-      <props>
-        <prop actor="props/units/tools/pitchfork.xml" attachpoint="r_hand"/>
-      </props>
-    </variant>
+    <variant file="units/variants/biped_attack_capture.xml"/>
     <variant name="attack_melee">
       <props>
         <prop actor="props/units/weapons/spear_hoplite.xml" attachpoint="r_hand"/>
Index: binaries/data/mods/public/art/actors/units/variants/biped_attack_capture.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/variants/biped_attack_capture.xml	(revision 0)
+++ binaries/data/mods/public/art/actors/units/variants/biped_attack_capture.xml	(working copy)
@@ -0,0 +1,10 @@
+<variant name="attack_capture">
+  <animations>
+	<animation event="0.4" file="infantry/sword/attack/isw_s_def_01.psa" name="attack_capture" speed="100"/>
+	<animation event="0.4" file="infantry/sword/attack/isw_s_def_06.psa" name="attack_capture" speed="100"/>
+	<animation event="0.2" file="infantry/sword/attack/isw_s_em_04.psa" name="attack_capture" speed="100"/>
+  </animations>
+  <props>
+	<prop actor="props/units/tools/pitchfork.xml" attachpoint="r_hand"/>
+  </props>
+</variant>
Index: source/graphics/ObjectBase.cpp
===================================================================
--- source/graphics/ObjectBase.cpp	(revision 16728)
+++ source/graphics/ObjectBase.cpp	(working copy)
@@ -38,6 +38,173 @@
 	m_Properties.m_FloatOnWater = false;
 }
 
+void CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant)
+{
+	#define EL(x) int el_##x = XeroFile.GetElementID(#x)
+	#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
+	EL(animation);
+	EL(animations);
+	EL(color);
+	EL(decal);
+	EL(mesh);
+	EL(particles);
+	EL(prop);
+	EL(props);
+	EL(texture);
+	EL(textures);
+	EL(variant);
+	AT(actor);
+	AT(angle);
+	AT(attachpoint);
+	AT(depth);
+	AT(event);
+	AT(file);
+	AT(frequency);
+	AT(load);
+	AT(maxheight);
+	AT(minheight);
+	AT(name);
+	AT(offsetx);
+	AT(offsetz);
+	AT(selectable);
+	AT(sound);
+	AT(speed);
+	AT(width);
+	#undef AT
+	#undef EL
+
+	if (variant.GetNodeName() != el_variant)
+	{
+		LOGERROR("Invalid variant format in (unrecognised root element '%s')", XeroFile.GetElementString(variant.GetNodeName()).c_str());
+		return;
+	}
+
+	XERO_ITER_ATTR(variant, attr)
+	{
+		if (attr.Name == at_name)
+			currentVariant.m_VariantName = attr.Value.LowerCase();
+		else if (attr.Name == at_frequency)
+			currentVariant.m_Frequency = attr.Value.ToInt();
+	}
+
+	XERO_ITER_EL(variant, option)
+	{
+		int option_name = option.GetNodeName();
+
+		if (option_name == el_mesh)
+		{
+			currentVariant.m_ModelFilename = VfsPath("art/meshes") / option.GetText().FromUTF8();
+		}
+		else if (option_name == el_textures)
+		{
+			XERO_ITER_EL(option, textures_element)
+			{
+				ENSURE(textures_element.GetNodeName() == el_texture);
+				
+				Samp samp;
+				XERO_ITER_ATTR(textures_element, se)
+				{
+					if (se.Name == at_file)
+						samp.m_SamplerFile = VfsPath("art/textures/skins") / se.Value.FromUTF8();
+					else if (se.Name == at_name)
+						samp.m_SamplerName = CStrIntern(se.Value);
+				}
+				currentVariant.m_Samplers.push_back(samp);
+			}
+		}
+		else if (option_name == el_decal)
+		{
+			XMBAttributeList attrs = option.GetAttributes();
+			Decal decal;
+			decal.m_SizeX = attrs.GetNamedItem(at_width).ToFloat();
+			decal.m_SizeZ = attrs.GetNamedItem(at_depth).ToFloat();
+			decal.m_Angle = DEGTORAD(attrs.GetNamedItem(at_angle).ToFloat());
+			decal.m_OffsetX = attrs.GetNamedItem(at_offsetx).ToFloat();
+			decal.m_OffsetZ = attrs.GetNamedItem(at_offsetz).ToFloat();
+			currentVariant.m_Decal = decal;
+		}
+		else if (option_name == el_particles)
+		{
+			XMBAttributeList attrs = option.GetAttributes();
+			VfsPath file = VfsPath("art/particles") / attrs.GetNamedItem(at_file).FromUTF8();
+			currentVariant.m_Particles = file;
+
+			// For particle hotloading, it's easiest to reload the entire actor,
+			// so remember the relevant particle file as a dependency for this actor
+			m_UsedFiles.insert(file);
+		}
+		else if (option_name == el_color)
+		{
+			currentVariant.m_Color = option.GetText();
+		}
+		else if (option_name == el_animations)
+		{
+			XERO_ITER_EL(option, anim_element)
+			{
+				ENSURE(anim_element.GetNodeName() == el_animation);
+
+				Anim anim;
+				XERO_ITER_ATTR(anim_element, ae)
+				{
+					if (ae.Name == at_name)
+					{
+						anim.m_AnimName = ae.Value;
+					}
+					else if (ae.Name == at_file)
+					{
+						anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8();
+					}
+					else if (ae.Name == at_speed)
+					{
+						anim.m_Speed = ae.Value.ToInt() / 100.f;
+						if (anim.m_Speed <= 0.0) anim.m_Speed = 1.0f;
+					}
+					else if (ae.Name == at_event)
+					{
+						float pos = ae.Value.ToFloat();
+						anim.m_ActionPos = clamp(pos, 0.f, 1.f);
+					}
+					else if (ae.Name == at_load)
+					{
+						float pos = ae.Value.ToFloat();
+						anim.m_ActionPos2 = clamp(pos, 0.f, 1.f);
+					}
+					else if (ae.Name == at_sound)
+					{
+						float pos = ae.Value.ToFloat();
+						anim.m_SoundPos = clamp(pos, 0.f, 1.f);
+					}
+				}
+				currentVariant.m_Anims.push_back(anim);
+			}
+
+		}
+		else if (option_name == el_props)
+		{
+			XERO_ITER_EL(option, prop_element)
+			{
+				ENSURE(prop_element.GetNodeName() == el_prop);
+
+				Prop prop;
+				XERO_ITER_ATTR(prop_element, pe)
+				{
+					if (pe.Name == at_attachpoint)
+						prop.m_PropPointName = pe.Value;
+					else if (pe.Name == at_actor)
+						prop.m_ModelName = pe.Value.FromUTF8();
+					else if (pe.Name == at_minheight)
+						prop.m_minHeight = pe.Value.ToFloat();
+					else if (pe.Name == at_maxheight)
+						prop.m_maxHeight = pe.Value.ToFloat();
+					else if (pe.Name == at_selectable)
+						prop.m_selectable = pe.Value != "false";
+				}
+				currentVariant.m_Props.push_back(prop);
+			}
+		}
+	}
+}
+
 bool CObjectBase::Load(const VfsPath& pathname)
 {
 	m_UsedFiles.clear();
@@ -53,36 +220,10 @@
 	EL(actor);
 	EL(castshadow);
 	EL(float);
+	EL(group);
 	EL(material);
-	EL(group);
 	EL(variant);
-	EL(animations);
-	EL(animation);
-	EL(props);
-	EL(prop);
-	EL(mesh);
-	EL(texture);
-	EL(textures);
-	EL(color);
-	EL(decal);
-	EL(particles);
 	AT(file);
-	AT(name);
-	AT(speed);
-	AT(event);
-	AT(load);
-	AT(sound);
-	AT(attachpoint);
-	AT(actor);
-	AT(frequency);
-	AT(width);
-	AT(depth);
-	AT(angle);
-	AT(offsetx);
-	AT(offsetz);
-	AT(minheight);
-	AT(maxheight);
-	AT(selectable);
 	#undef AT
 	#undef EL
 
@@ -94,7 +235,6 @@
 		return false;
 	}
 
-
 	m_VariantGroups.clear();
 
 	m_Pathname = pathname;
@@ -135,152 +275,39 @@
 				ENSURE(variant.GetNodeName() == el_variant);
 				XERO_ITER_ATTR(variant, attr)
 				{
-					if (attr.Name == at_name)
-						currentVariant->m_VariantName = attr.Value.LowerCase();
-
-					else if (attr.Name == at_frequency)
-						currentVariant->m_Frequency = attr.Value.ToInt();
-				}
-
-				XERO_ITER_EL(variant, option)
-				{
-					int option_name = option.GetNodeName();
-
-					if (option_name == el_mesh)
+					if (attr.Name == at_file)
 					{
-						currentVariant->m_ModelFilename = VfsPath("art/meshes") / option.GetText().FromUTF8();
-					}
-					else if (option_name == el_textures)
-					{
-						XERO_ITER_EL(option, textures_element)
+						// Open up an external file to load.
+						// Don't crash hard when failures happen, but log them and continue
+						m_UsedFiles.insert(attr.Value);
+						CXeromyces XeroVariant;
+						if (XeroVariant.Load(g_VFS, "art/actors/" + attr.Value) == PSRETURN_OK)
 						{
-							ENSURE(textures_element.GetNodeName() == el_texture);
-							
-							Samp samp;
-							XERO_ITER_ATTR(textures_element, se)
-							{
-								if (se.Name == at_file)
-									samp.m_SamplerFile = VfsPath("art/textures/skins") / se.Value.FromUTF8();
-								else if (se.Name == at_name)
-									samp.m_SamplerName = CStrIntern(se.Value);
-							}
-							currentVariant->m_Samplers.push_back(samp);
+							XMBElement variantRoot = XeroVariant.GetRoot();
+							LoadVariant(XeroVariant, variantRoot, *currentVariant);
 						}
+						else
+							LOGERROR("Could not open path %s", attr.Value);
 					}
-					else if (option_name == el_decal)
-					{
-						XMBAttributeList attrs = option.GetAttributes();
-						Decal decal;
-						decal.m_SizeX = attrs.GetNamedItem(at_width).ToFloat();
-						decal.m_SizeZ = attrs.GetNamedItem(at_depth).ToFloat();
-						decal.m_Angle = DEGTORAD(attrs.GetNamedItem(at_angle).ToFloat());
-						decal.m_OffsetX = attrs.GetNamedItem(at_offsetx).ToFloat();
-						decal.m_OffsetZ = attrs.GetNamedItem(at_offsetz).ToFloat();
-						currentVariant->m_Decal = decal;
-					}
-					else if (option_name == el_particles)
-					{
-						XMBAttributeList attrs = option.GetAttributes();
-						VfsPath file = VfsPath("art/particles") / attrs.GetNamedItem(at_file).FromUTF8();
-						currentVariant->m_Particles = file;
-
-						// For particle hotloading, it's easiest to reload the entire actor,
-						// so remember the relevant particle file as a dependency for this actor
-						m_UsedFiles.insert(file);
-					}
-					else if (option_name == el_color)
-					{
-						currentVariant->m_Color = option.GetText();
-					}
-					else if (option_name == el_animations)
-					{
-						XERO_ITER_EL(option, anim_element)
-						{
-							ENSURE(anim_element.GetNodeName() == el_animation);
-
-							Anim anim;
-							XERO_ITER_ATTR(anim_element, ae)
-							{
-								if (ae.Name == at_name)
-								{
-									anim.m_AnimName = ae.Value;
-								}
-								else if (ae.Name == at_file)
-								{
-									anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8();
-								}
-								else if (ae.Name == at_speed)
-								{
-									anim.m_Speed = ae.Value.ToInt() / 100.f;
-									if (anim.m_Speed <= 0.0) anim.m_Speed = 1.0f;
-								}
-								else if (ae.Name == at_event)
-								{
-									float pos = ae.Value.ToFloat();
-									anim.m_ActionPos = clamp(pos, 0.f, 1.f);
-								}
-								else if (ae.Name == at_load)
-								{
-									float pos = ae.Value.ToFloat();
-									anim.m_ActionPos2 = clamp(pos, 0.f, 1.f);
-								}
-								else if (ae.Name == at_sound)
-								{
-									float pos = ae.Value.ToFloat();
-									anim.m_SoundPos = clamp(pos, 0.f, 1.f);
-								}
-							}
-							currentVariant->m_Anims.push_back(anim);
-						}
-
-					}
-					else if (option_name == el_props)
-					{
-						XERO_ITER_EL(option, prop_element)
-						{
-							ENSURE(prop_element.GetNodeName() == el_prop);
-
-							Prop prop;
-							XERO_ITER_ATTR(prop_element, pe)
-							{
-								if (pe.Name == at_attachpoint)
-									prop.m_PropPointName = pe.Value;
-								else if (pe.Name == at_actor)
-									prop.m_ModelName = pe.Value.FromUTF8();
-								else if (pe.Name == at_minheight)
-									prop.m_minHeight = pe.Value.ToFloat();
-								else if (pe.Name == at_maxheight)
-									prop.m_maxHeight = pe.Value.ToFloat();
-								else if (pe.Name == at_selectable)
-									prop.m_selectable = pe.Value != "false";
-							}
-							currentVariant->m_Props.push_back(prop);
-						}
-					}
 				}
 
+				// Always expand the contents of this node, even if a file has
+				// been loaded to allow for extra definitions
+				LoadVariant(XeroFile, variant, *currentVariant);
 				++currentVariant;
 			}
 
 			if (currentGroup->size() == 0)
-			{
 				LOGERROR("Actor group has zero variants ('%s')", pathname.string8());
-			}
 
 			++currentGroup;
 		}
 		else if (child_name == el_castshadow)
-		{
 			m_Properties.m_CastShadows = true;
-		}
 		else if (child_name == el_float)
-		{
 			m_Properties.m_FloatOnWater = true;
-		}
 		else if (child_name == el_material)
-		{
 			m_Material = VfsPath("art/materials") / child.GetText().FromUTF8();
-		}
 	}
 
 	if (m_Material.empty())
Index: source/graphics/ObjectBase.h
===================================================================
--- source/graphics/ObjectBase.h	(revision 16728)
+++ source/graphics/ObjectBase.h	(working copy)
@@ -29,6 +29,7 @@
 #include "lib/file/vfs/vfs_path.h"
 #include "ps/CStr.h"
 #include "ps/CStrIntern.h"
+#include "ps/XML/Xeromyces.h"
 
 #include <boost/random/mersenne_twister.hpp>
 
@@ -187,6 +188,8 @@
 	CObjectManager& m_ObjectManager;
 
 	boost::unordered_set<VfsPath> m_UsedFiles;
+
+	void LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant);
 };
 
 #endif
