Index: binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_details.js	(révision 13775)
+++ binaries/data/mods/public/gui/session/selection_details.js	(copie de travail)
@@ -166,6 +166,15 @@
 		getGUIObjectByName("resourceCarryingText").caption = entState.resourceSupply.gatherers.length + " / " + entState.resourceSupply.maxGatherers + "    ";
 		getGUIObjectByName("resourceCarryingIcon").tooltip = "Current/max gatherers";
 	}
+	// And for number of corraled animals
+	else if (entState.corral)
+	{
+		getGUIObjectByName("resourceCarryingIcon").hidden = false;
+		getGUIObjectByName("resourceCarryingText").hidden = false;
+		getGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/portraits/structures/corral.png";
+		getGUIObjectByName("resourceCarryingText").caption = entState.corral.entities.length + " / " + entState.corral.capacity + "    ";
+		getGUIObjectByName("resourceCarryingIcon").tooltip = "Current/max corraled animals";
+	}
 	else
 	{
 		getGUIObjectByName("resourceCarryingIcon").hidden = true;
Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(révision 13775)
+++ binaries/data/mods/public/gui/session/input.js	(copie de travail)
@@ -248,6 +248,16 @@
 			if (targetState.garrisonHolder.entities.length >= targetState.garrisonHolder.capacity)
 				tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
 		}
+		else if (targetState.corral)
+		{
+			data.command = "corral";
+			data.target = target;
+			cursor = "action-gather-milk";
+			tooltip = "Current corraled: " + targetState.corral.entities.length
+				+ "/" + targetState.corral.capacity;
+			if (targetState.corral.entities.length >= targetState.corral.capacity)
+				tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
+		}
 		else if (targetState.resourceSupply)
 		{
 			var resourceType = targetState.resourceSupply.type;
@@ -349,6 +359,16 @@
 				}
 			}
 			break;
+		case "corral":
+			if (hasClass(entState, "Unit") && targetState.corral)
+			{
+				var tooltip = "Current corraled: " + targetState.corral.entities.length
+					+ "/" + targetState.corral.capacity;
+				if (targetState.corral.entities.length >= targetState.corral.capacity)
+					tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
+				return {"possible": Engine.GuiInterfaceCall("CanBeCorraled", {"entity": entState.id, "corral": target}), "tooltip": tooltip};
+			}
+			break;
 		case "setup-trade-route":
 			// If ground or sea trade possible
 			if (!targetState.foundation && ((entState.trader && hasClass(entState, "Organic") && (playerOwned || allyOwned) && hasClass(targetState, "Market")) ||
@@ -515,6 +535,10 @@
 	{
 		return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target};
 	}
+	else if (Engine.HotkeyIsPressed("session.corral") && (actionInfo = getActionInfo("corral", target)).possible)
+	{
+		return {"type": "corral", "cursor": "action-gather-milk", "tooltip": actionInfo.tooltip, "target": target};
+	}
 	else if (Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible)
 	{
 			return {"type": "attack-move", "cursor": "action-attack-move"};
@@ -1397,6 +1421,11 @@
 		Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] });
 		return true;
 
+	case "corral":
+		Engine.PostNetworkCommand({"type": "corral", "entities": selection, "target": action.target, "queued": queued});
+		Engine.GuiInterfaceCall("PlaySound", { "name": "order_corral", "entity": selection[0] });
+		return true;
+
 	case "set-rallypoint":
 		var pos = undefined;
 		// if there is a position set in the action then use this so that when setting a 
Index: binaries/data/mods/public/gui/session/session.xml
===================================================================
--- binaries/data/mods/public/gui/session/session.xml	(révision 13775)
+++ binaries/data/mods/public/gui/session/session.xml	(copie de travail)
@@ -997,7 +997,7 @@
 			    <object size="90 -2 126 34" name="attackAndArmorStats" type="image" sprite="stretched:session/icons/stances/defensive.png" tooltip="Attack and Armor" tooltip_style="sessionToolTip"/>
 
 			    <!-- Resource carrying icon/counter -->
-			    <!-- Used also for number of gatherers/builders -->
+			    <!-- Used also for number of gatherers/builders/corraled -->
 			    <object size="100%-98 -2 100%-28 34" type="text" name="resourceCarryingText" style="CarryingTextRight"/>
 			    <object size="100%-36 -2 100% 34" type="image" name="resourceCarryingIcon" tooltip_style="sessionToolTip"/>
 			</object>
Index: binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml	(révision 13775)
+++ binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml	(copie de travail)
@@ -10,14 +10,10 @@
     <Square width="12.0" depth="22.0"/>
     <Height>5.0</Height>
   </Footprint>
-  <GarrisonHolder>
-    <Max>10</Max>
-    <EjectHealth>0.1</EjectHealth>
-    <EjectEntitiesOnDestroy>true</EjectEntitiesOnDestroy>
-    <List datatype="tokens">Animal</List>
-    <BuffHeal>1</BuffHeal>
-    <LoadingRange>2</LoadingRange>
-  </GarrisonHolder>
+  <Corral>
+    <MaxCapacity>10</MaxCapacity>
+    <Range>6</Range>
+  </Corral>
   <Health>
     <Max>500</Max>
     <SpawnEntityOnDeath>rubble/rubble_2x4</SpawnEntityOnDeath>
Index: binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml	(révision 13775)
+++ binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml	(copie de travail)
@@ -12,6 +12,10 @@
 	<Classes datatype="tokens">Domestic</Classes>
     <Icon>gaia/fauna_sheep.png</Icon>
   </Identity>
+  <UnitAI>
+    <CorralType>food</CorralType>
+    <CorralRate>0.4</CorralRate>
+  </UnitAI>
   <Position>
     <Anchor>pitch</Anchor>
   </Position>
Index: binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml	(révision 13775)
+++ binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml	(copie de travail)
@@ -5,6 +5,10 @@
     <SpecificName>Pig</SpecificName>
     <Icon>gaia/fauna_pig.png</Icon>
   </Identity>
+  <UnitAI>
+    <CorralType>food</CorralType>
+    <CorralRate>0.4</CorralRate>
+  </UnitAI>
   <Position>
     <Anchor>pitch</Anchor>
   </Position>
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(révision 13775)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(copie de travail)
@@ -319,6 +319,12 @@
 		}
 		break;
 
+	case "corral":
+		GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
+			cmpUnitAI.SetCorraled(cmd.target, cmd.queued);
+		});
+		break;
+
 	case "stop":
 		GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
 			cmpUnitAI.Stop(cmd.queued);
Index: binaries/data/mods/public/simulation/helpers/RallyPointCommands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/RallyPointCommands.js	(révision 13775)
+++ binaries/data/mods/public/simulation/helpers/RallyPointCommands.js	(copie de travail)
@@ -60,6 +60,14 @@
 				"queued": true
 			});
 			break;
+		case "corral": 
+			ret.push( {
+				"type": "corral",
+				"entities": spawnedEnts,
+				"target": data[i].target,
+				"queued": true
+			});
+			break;
 		case "trade":
 			ret.push( {
 				"type": "setup-trade-route",
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js	(révision 13775)
+++ binaries/data/mods/public/simulation/components/GuiInterface.js	(copie de travail)
@@ -291,18 +291,27 @@
 	}
 
 	var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
-	if (cmpResourceSupply)
+	if (cmpResourceSupply)
+	{
+		ret.resourceSupply = {
+			"isInfinite": cmpResourceSupply.IsInfinite(),
+			"max": cmpResourceSupply.GetMaxAmount(),
+			"amount": cmpResourceSupply.GetCurrentAmount(),
+			"type": cmpResourceSupply.GetType(),
+			"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
+			"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
+			"gatherers": cmpResourceSupply.GetGatherers()
+		};
+	}
+
+	var cmpCorral = Engine.QueryInterface(ent, IID_Corral);
+	if (cmpCorral)
 	{
-		ret.resourceSupply = {
-			"isInfinite": cmpResourceSupply.IsInfinite(),
-			"max": cmpResourceSupply.GetMaxAmount(),
-			"amount": cmpResourceSupply.GetCurrentAmount(),
-			"type": cmpResourceSupply.GetType(),
-			"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
-			"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
-			"gatherers": cmpResourceSupply.GetGatherers()
-		};
-	}
+		ret.corral = {
+			"entities": cmpCorral.GetEntities(),
+			"capacity": cmpCorral.GetCapacity()
+		};
+	}
 
 	var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
 	if (cmpResourceGatherer)
@@ -1716,6 +1725,15 @@
 	return cmpAttack.CanAttack(data.target);
 };
 
+GuiInterface.prototype.CanBeCorraled = function(player, data)
+{
+	var cmpCorral = Engine.QueryInterface(data.corral, IID_Corral);
+	if (!cmpCorral)
+		return false;
+
+	return cmpCorral.CanBeCorraled(data.entity);
+};
+
 /*
  * Returns batch build time.
  */
@@ -1801,6 +1819,7 @@
 	"GetTradingRouteGain": 1,
 	"GetTradingDetails": 1,
 	"CanAttack": 1,
+	"CanBeCorraled": 1,
 	"GetBatchTime": 1,
 
 	"SetPathfinderDebugOverlay": 1,
Index: binaries/data/mods/public/simulation/components/interfaces/Corral.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/Corral.js	(révision 0)
+++ binaries/data/mods/public/simulation/components/interfaces/Corral.js	(copie de travail)
@@ -0,0 +1,5 @@
+Engine.RegisterInterface("Corral");
+
+// Message of the form { },
+// sent to the current entity whenever the corraled units change.
+Engine.RegisterMessageType("CorraledUnitsChanged");
Index: binaries/data/mods/public/simulation/components/Corral.js
===================================================================
--- binaries/data/mods/public/simulation/components/Corral.js	(révision 0)
+++ binaries/data/mods/public/simulation/components/Corral.js	(copie de travail)
@@ -0,0 +1,162 @@
+function Corral() {}
+
+Corral.prototype.Schema =
+	"<element name='MaxCapacity' a:help='Maximum number of corraled units'>" +
+		"<ref name='nonNegativeDecimal'/>" +
+	"</element>" +
+	"<element name='Range' a:help='Range distance of corraled units'>" +
+		"<ref name='nonNegativeDecimal'/>" +
+	"</element>";
+
+Corral.prototype.Init = function()
+{
+	this.entities = [];
+	this.increment = 1;
+	this.timer = undefined;
+};
+
+Corral.prototype.GetCapacity = function()
+{
+	return +this.template.MaxCapacity;
+};
+
+Corral.prototype.GetEntities = function()
+{
+	return this.entities;
+};
+
+Corral.prototype.GetRange = function()
+{
+	return +this.template.Range;
+};
+
+/**
+ * Returns true if the entity ent can be corraled inside,
+ */
+Corral.prototype.CanBeCorraled = function(ent)
+{
+	var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+	if (!cmpUnitAI)
+		return false;
+	// Formation controllers should always respond to commands
+	if (cmpUnitAI.IsFormationController())
+		return true;
+
+	// The entity must have a corral type not null
+	var entType = cmpUnitAI.GetCorralType();
+	if (entType == null)
+		return false;
+	// And if there is already a corraled unit, it must have the same type
+	var type = this.GetType();
+	if (type != null && type != entType)
+		return false;
+	return true;
+};
+
+Corral.prototype.AddCorraled = function(ent)
+{
+	if (this.entities.indexOf(ent) != -1 || this.entities.length >= this.GetCapacity())
+		return false;
+	this.entities.push(ent);
+	Engine.PostMessage(this.entity, MT_CorraledUnitsChanged, {});
+	return true;
+};
+
+Corral.prototype.RemoveCorraled = function(ent)
+{
+	if (this.entities.indexOf(ent) == -1)
+		return;
+	this.entities.splice(this.entities.indexOf(ent), 1);
+	Engine.PostMessage(this.entity, MT_CorraledUnitsChanged, {});
+};
+
+Corral.prototype.GetRate = function()
+{
+	var rate = 0;
+	for each (var ent in this.entities)
+	{
+		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+		rate += cmpUnitAI.GetCorralRate();
+	}
+	rate = ApplyTechModificationsToEntity("Corral/Rate", rate, this.entity);
+	return rate;
+}
+
+Corral.prototype.GetType = function()
+{
+	var type = null;
+	if (this.entities.length > 0)
+	{
+		var cmpUnitAI = Engine.QueryInterface(this.entities[0], IID_UnitAI);
+		type= cmpUnitAI.GetCorralType();
+	}
+	return type;
+}
+
+/**
+ * Called every second. Produce food (in corrals)
+ */
+Corral.prototype.Timeout = function(data)
+{
+	var type = this.GetType();
+	var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+	if (type != null)
+	{
+		var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+		cmpPlayer.AddResource(type, this.increment);
+		var cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
+		if (cmpStatisticsTracker)
+			cmpStatisticsTracker.IncreaseResourceGatheredCounter(type, this.increment);
+
+		var rate = this.GetRate();
+		this.increment = this.entities.length;
+		this.timer = cmpTimer.SetTimeout(this.entity, IID_Corral, "Timeout", this.increment*1000/rate, {});
+	}
+	else
+	{
+		cmpTimer.CancelTimer(this.timer);
+		this.timer = undefined;
+	}
+};
+
+
+Corral.prototype.OnCorraledUnitsChanged = function(msg)
+{
+	// first clear the cache for technology modifications (the total rate has changed)
+	var cmpTechMan = QueryOwnerInterface(this.entity, IID_TechnologyManager);
+	if (cmpTechMan)
+		cmpTechMan.clearModificationCache(this.entity);
+
+	if (this.entities.length > 0 && !this.timer)
+	{
+		var rate = this.GetRate();
+		this.increment = this.entities.length;
+		var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+		this.timer = cmpTimer.SetTimeout(this.entity, IID_Corral, "Timeout", this.increment*1000/rate, {});
+	}
+	else if (this.entities.length == 0 && this.timer)
+	{
+		var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+		cmpTimer.CancelTimer(this.timer);
+		this.timer = undefined;
+	}
+};
+
+Corral.prototype.OnDestroy = function()
+{
+	if (this.timer)
+	{
+		var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+		cmpTimer.CancelTimer(this.timer);
+	}
+
+	for (var i = this.entities.length; i > 0; --i)
+	{
+		var cmpUnitAI = Engine.QueryInterface(this.entities[i-1], IID_UnitAI);
+		if (cmpUnitAI)
+			cmpUnitAI.UnsetCorraled();
+	}
+};
+
+
+Engine.RegisterComponentType(IID_Corral, "Corral", Corral);
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(révision 13775)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(copie de travail)
@@ -46,6 +46,19 @@
 				"<ref name='positiveDecimal'/>" +
 			"</element>"+
 		"</interleave>" +
+	"</optional>" +
+	"<optional>" +
+		"<interleave>" +
+			"<element name='CorralType' a:help='Production type when corraled'>" +
+				"<choice>" +
+					"<value>food</value>" +
+					"<value>metal</value>" +
+				"</choice>" +
+			"</element>" +
+			"<element name='CorralRate' a:help='Production rate per sec when corraled'>" +
+				"<ref name='positiveDecimal'/>" +
+			"</element>" +
+		"</interleave>" +
 	"</optional>";
 
 // Unit stances.
@@ -620,6 +633,26 @@
 		}
 	},
 
+	"Order.Corral": function(msg) {
+		var cmpCorral = Engine.QueryInterface(msg.data.target, IID_Corral);
+		if (!cmpCorral)
+		{
+			this.FinishOrder();
+			return;
+		}
+
+		var range = cmpCorral.GetRange();
+		if (this.MoveToTargetRangeExplicit(this.order.data.target, 0, range))
+		{
+			this.SetNextState("ANIMAL.CORRAL.APPROACHING");
+		}
+		else
+		{
+			this.StopMoving();
+			this.SetNextState("ANIMAL.CORRAL.CORRALED");
+		}
+	},
+
 	"Order.Cheering": function(msg) {
 		this.SetNextState("INDIVIDUAL.CHEERING");
 	},
@@ -905,6 +938,14 @@
 			this.SetNextStateAlwaysEntering("MEMBER");
 		},
 
+		"Order.Corral": function(msg) {
+			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+			cmpFormation.SetRearrange(false);
+			cmpFormation.CallMemberFunction("SetCorraled", [msg.data.target, false]);
+
+			this.SetNextStateAlwaysEntering("MEMBER");
+		},
+
 		"IDLE": {
 		},
 
@@ -2382,6 +2423,27 @@
 			this.SetNextState("FLEEING");
 		},
 
+
+		"Order.Corral": function(msg) {
+			var cmpCorral = Engine.QueryInterface(msg.data.target, IID_Corral);
+			if (!cmpCorral)
+			{
+				this.FinishOrder();
+				return;
+			}
+
+			var range = cmpCorral.GetRange();
+			if (this.MoveToTargetRangeExplicit(this.order.data.target, 0, range))
+			{
+				this.SetNextState("ANIMAL.CORRAL.APPROACHING");
+			}
+			else
+			{
+				this.StopMoving();
+				this.SetNextState("ANIMAL.CORRAL.CORRALED");
+			}
+		},
+
 		"IDLE": {
 			// (We need an IDLE state so that FinishOrder works)
 
@@ -2392,6 +2454,53 @@
 			},
 		},
 
+		'CORRAL': {
+			'APPROACHING': {
+				"MoveCompleted": function() {
+					var cmpCorral = Engine.QueryInterface(this.order.data.target, IID_Corral);
+					if (cmpCorral)
+					{
+						var range = cmpCorral.GetRange();
+						var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+						if (cmpUnitMotion.IsInTargetRange(this.order.data.target, 0, range))
+						{
+							this.SetNextState("ANIMAL.CORRAL.CORRALED");
+							return;
+						}						
+					}
+					this.FinishOrder();
+				},
+			},
+
+			'CORRALED': {
+				"enter": function() {
+					this.corral = this.order.data.target;
+					var cmpCorral = Engine.QueryInterface(this.corral, IID_Corral);
+					if (!cmpCorral || !cmpCorral.AddCorraled(this.entity))
+					{
+						this.SetNextState("ROAMING");
+						return true;
+					}
+					this.SelectAnimation("feeding");
+					return false;
+				},
+
+				"Order.UnsetCorraled": function() {
+					this.FinishOrder();
+				},
+
+				"leave": function() {
+					if (this.corral)
+					{
+						var cmpCorral = Engine.QueryInterface(this.corral, IID_Corral);
+						if (cmpCorral)
+							cmpCorral.RemoveCorraled(this.entity);
+						delete this.corral;
+					}
+				},
+			},
+		},
+
 		"ROAMING": {
 			"enter": function() {
 				// Walk in a random direction
@@ -3781,6 +3890,7 @@
 		case "ReturnResource":
 		case "Repair":
 		case "Garrison":
+		case "Corral":
 			// Find the target unit's position
 			var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
 			if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
@@ -4521,6 +4631,39 @@
 
 //// Animal specific functions ////
 
+/**
+ * Adds setcorraled order to the queue, forced by the player.
+ */
+UnitAI.prototype.SetCorraled = function(target, queued)
+{
+	var cmpCorral = Engine.QueryInterface(target, IID_Corral);
+	if (!cmpCorral || !cmpCorral.CanBeCorraled(this.entity))
+	{
+		this.WalkToTarget(target, queued);
+		return;
+	}
+	this.AddOrder("Corral", { "target": target, "force": true }, queued);
+};
+
+/**
+ * Adds unsetcorraled order to the queue.
+ */
+UnitAI.prototype.UnsetCorraled = function()
+{
+	if (this.corral)
+		this.AddOrder("UnsetCorraled", null, false);
+};
+
+UnitAI.prototype.GetCorralRate = function()
+{
+	return +(this.template.CorralRate || 0);
+};
+
+UnitAI.prototype.GetCorralType = function()
+{
+	return (this.template.CorralType || null);
+};
+
 UnitAI.prototype.MoveRandomly = function(distance)
 {
 	// We want to walk in a random direction, but avoid getting stuck
Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg	(révision 13775)
+++ binaries/data/config/default.cfg	(copie de travail)
@@ -270,6 +270,7 @@
 hotkey.session.attack = "Ctrl+Alt"          ; Modifier to force attack instead of another action
 hotkey.session.garrison = Ctrl              ; Modifier to garrison when clicking on building
 hotkey.session.attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
+hotkey.session.corral = Ctrl                ; Modifier to corral an animal
 hotkey.session.queue = Shift                ; Modifier to queue unit orders instead of replacing
 hotkey.session.batchtrain = Shift           ; Modifier to train units in batches
 hotkey.session.massbarter = Shift           ; Modifier to barter bunch of resources
