Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg	(révision 14234)
+++ binaries/data/config/default.cfg	(copie de travail)
@@ -272,6 +272,7 @@
 hotkey.session.attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
 hotkey.session.garrison = Ctrl              ; Modifier to garrison when clicking on building
 hotkey.session.autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
+hotkey.session.guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
 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
@@ -300,6 +301,8 @@
 hotkey.menu.toggle = "F10"                   ; Toggle in-game menu
 hotkey.timeelapsedcounter.toggle = "F12"     ; Toggle time elapsed counter
 hotkey.session.showstatusbars = Tab          ; Toggle display of status bars
+hotkey.session.highlightguarding = PgDn      ; Toggle highlight of guarding units
+hotkey.session.highlightguarded = PgUp       ; Toggle highlight of guarded units
 
 ; > HOTKEYS ONLY
 hotkey.chat = Return                        ; Toggle chat window
Index: binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png
===================================================================
Impossible d'afficher : fichier considéré comme binaire.
svn:mime-type = application/octet-stream
Index: binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png
===================================================================
--- binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png	(révision 14234)
+++ binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png	(copie de travail)

Modification de propriétés sur binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: binaries/data/mods/public/art/textures/cursors/action-guard-disabled.txt
===================================================================
--- binaries/data/mods/public/art/textures/cursors/action-guard-disabled.txt	(révision 0)
+++ binaries/data/mods/public/art/textures/cursors/action-guard-disabled.txt	(copie de travail)
@@ -0,0 +1 @@
+1 1
Index: binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png
===================================================================
Impossible d'afficher : fichier considéré comme binaire.
svn:mime-type = application/octet-stream
Index: binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png
===================================================================
--- binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png	(révision 14234)
+++ binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png	(copie de travail)

Modification de propriétés sur binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png
===================================================================
Impossible d'afficher : fichier considéré comme binaire.
svn:mime-type = application/octet-stream
Index: binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png
===================================================================
--- binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png	(révision 14234)
+++ binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png	(copie de travail)

Modification de propriétés sur binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(révision 14234)
+++ binaries/data/mods/public/gui/session/input.js	(copie de travail)
@@ -15,6 +15,7 @@
 const ACTION_NONE = 0;
 const ACTION_GARRISON = 1;
 const ACTION_REPAIR = 2;
+const ACTION_GUARD = 3;
 var preSelectedAction = ACTION_NONE;
 
 const INPUT_NORMAL = 0;
@@ -426,6 +427,10 @@
 			if (entState.attack && targetState.hitpoints && (enemyOwned || neutralOwned))
 				return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})};
 			break;
+		case "guard":
+			if (targetState.guard && (playerOwned || mutualAllyOwned))
+				return {"possible": (entState.unitAI && entState.unitAI.canGuard)};
+			break;
 		}
 	}
 	if (action == "move" || action == "attack-move")
@@ -502,6 +507,12 @@
 			else
 				return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
 			break;
+		case ACTION_GUARD:
+			if (getActionInfo("guard", target).possible)
+				return {"type": "guard", "cursor": "action-guard", "target": target};
+			else
+				return {"type": "none", "cursor": "action-guard-disabled", "target": undefined};
+			break;
 		}
 	}
 	else if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
@@ -516,6 +527,10 @@
 	{
 			return {"type": "attack-move", "cursor": "action-attack-move"};
 	}
+	else if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible)
+	{
+		return {"type": "guard", "cursor": "action-guard", "target": target};
+	}
 	else
 	{
 		if ((actionInfo = getActionInfo("setup-trade-route", target)).possible)
@@ -1053,6 +1068,18 @@
 		recalculateStatusBarDisplay();
 	}
 
+	if (ev.hotkey == "session.highlightguarding" && g_ShowGuarding != (ev.type == "hotkeydown"))
+	{
+		g_ShowGuarding = (ev.type == "hotkeydown");
+		updateHighlightSelectionGuards();
+	}
+
+	if (ev.hotkey == "session.highlightguarded" && g_ShowGuarded != (ev.type == "hotkeydown"))
+	{
+		g_ShowGuarded = (ev.type == "hotkeydown");
+		updateHighlightSelectionGuards();
+	}
+
 	// State-machine processing:
 
 	switch (inputState)
@@ -1394,6 +1421,11 @@
 		Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] });
 		return true;
 
+	case "guard":
+		Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued});
+		Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "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 
@@ -1820,6 +1852,13 @@
 				inputState = INPUT_PRESELECTEDACTION;
 				preSelectedAction = ACTION_REPAIR;
 				break;
+			case "add-guard":
+				inputState = INPUT_PRESELECTEDACTION;
+				preSelectedAction = ACTION_GUARD;
+				break;
+			case "remove-guard":
+				removeGuard();
+				break;
 			case "unload-all":
 				unloadAll();
 				break;
@@ -2106,6 +2145,17 @@
 	
 }
 
+function removeGuard()
+{
+	// Filter out all entities that are currently guarding/escorting.
+	var entities = g_Selection.toList().filter(function(e) {
+		var state = GetEntityState(e);
+		return (state && state.unitAI && state.unitAI.isGuarding);
+	});
+	
+	Engine.PostNetworkCommand({"type": "remove-guard", "entities": entities});
+}
+
 function clearSelection()
 {
 	if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js	(révision 14234)
+++ binaries/data/mods/public/gui/session/session.js	(copie de travail)
@@ -46,6 +46,11 @@
 const DEFAULT_POPULATION_COLOR = "white";
 const POPULATION_ALERT_COLOR = "orange";
 
+// List of additionnal entities to highlight
+var g_ShowGuarding = false;
+var g_ShowGuarded = false;
+var g_AdditionnalHighlight = [];
+
 function GetSimState()
 {
 	if (!g_SimState)
@@ -331,6 +336,10 @@
 
 		// Display rally points for selected buildings
 		Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
+
+		// Update the highlight of guards if needed
+		if (g_ShowGuarding || g_ShowGuarded)
+			updateHighlightSelectionGuards();
 	}
 
 	// Run timers
@@ -665,6 +674,51 @@
 	Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
 }
 
+// Update the additionnal list of entities to be highlighted.
+function updateHighlightSelectionGuards()
+{
+	var guarding = [];  // guarded units will be highlighted with the player color
+	var guarded  = [];  // guarding units will be highlighted in black 
+	var remove = [];
+	var selected = g_Selection.toList();
+
+	if (g_ShowGuarding)
+	{
+		// flag the guarding entities to add in this additionnal highlight
+		for each (var sel in selected)
+		{
+			var state = GetEntityState(sel);
+			if (!state.guard || !state.guard.entities.length)
+				continue;
+			for each (var ent in state.guard.entities)
+				if (selected.indexOf(ent) == -1 && guarding.indexOf(ent) == -1)
+					guarding.push(ent);
+		}
+	}
+
+	if (g_ShowGuarded)
+	{
+		// flag the guarded entities to add in this additionnal highlight
+		for each (var sel in selected)
+		{
+			var state = GetEntityState(sel);
+			if (!state.unitAI || !state.unitAI.isGuarding)
+				continue;
+			var ent = state.unitAI.isGuarding;
+			if (selected.indexOf(ent) == -1 && guarding.indexOf(ent) == -1 && guarded.indexOf(ent) == -1)
+				guarded.push(ent);
+		}
+	}
+
+	// flag the entities to remove (from the previously added) from this additionnal highlight
+	for each (var ent in g_AdditionnalHighlight)
+	    if (selected.indexOf(ent) == -1 && guarding.indexOf(ent) == -1 && guarded.indexOf(ent) == -1 && remove.indexOf(ent) == -1)
+			remove.push(ent);
+
+	Engine.GuiInterfaceCall("SetAdditionnalHighlight", { "entitiesBlack": guarding, "entitiesColor": guarded, "entitiesRemove": remove });
+	g_AdditionnalHighlight = guarding.concat(guarded);
+}
+
 // Temporarily adding this here
 const AMBIENT_TEMPERATE = "temperate";
 var currentAmbient;
Index: binaries/data/mods/public/gui/session/utility_functions.js
===================================================================
--- binaries/data/mods/public/gui/session/utility_functions.js	(révision 14234)
+++ binaries/data/mods/public/gui/session/utility_functions.js	(copie de travail)
@@ -303,6 +303,24 @@
 		});
 	}
 
+	if (entState.unitAI && !entState.unitAI.isGuarding && entState.unitAI.canGuard)
+	{
+		commands.push({
+		    "name": "add-guard",
+		    "tooltip": "Guard",
+		    "icon": "add-guard.png"
+		});
+	}
+
+	if (entState.unitAI && entState.unitAI.isGuarding)
+	{
+		commands.push({
+		    "name": "remove-guard",
+		    "tooltip": "Remove guard",
+		    "icon": "remove-guard.png"
+		});
+	}
+
 	return commands;
 }
 
Index: binaries/data/mods/public/simulation/components/Guard.js
===================================================================
--- binaries/data/mods/public/simulation/components/Guard.js	(révision 0)
+++ binaries/data/mods/public/simulation/components/Guard.js	(copie de travail)
@@ -0,0 +1,107 @@
+function Guard() {}
+
+Guard.prototype.Schema =
+	"<empty/>";
+
+Guard.prototype.Init = function()
+{
+	this.entities = [];
+};
+
+Guard.prototype.GetRange = function(entity)
+{
+	var range = 8;
+	var cmpFootprint = Engine.QueryInterface(entity, IID_Footprint);
+	if (cmpFootprint)
+	{
+		var shape = cmpFootprint.GetShape();
+		if (shape.type == "square")
+			range += Math.sqrt(shape.depth*shape.depth + shape.width*shape.width)*2/3;
+		else if (shape.type == "circle")
+			range += shape.radius*3/2;
+	}
+	return range;
+};
+
+Guard.prototype.GetEntities = function()
+{
+	return this.entities.slice();
+};
+
+Guard.prototype.SetEntities = function(entities)
+{
+	this.entities = entities;
+};
+
+Guard.prototype.AddGuard = function(ent)
+{
+	if (this.entities.indexOf(ent) != -1)
+		return;
+	this.entities.push(ent); 
+};
+
+Guard.prototype.RemoveGuard = function(ent)
+{
+	var index = this.entities.indexOf(ent);
+	if (index != -1)
+		this.entities.splice(index, 1);
+};
+
+Guard.prototype.OnAttacked = function(msg)
+{
+	for each (var ent in this.entities)
+		Engine.PostMessage(ent, MT_GuardedAttacked, { "guarded": this.entity, "data": msg });
+};
+
+/**
+ * Update list of guarding/escorting entities if one gets renamed (e.g. by promotion)
+ */
+Guard.prototype.OnGlobalEntityRenamed = function(msg)
+{
+	var entityIndex = this.entities.indexOf(msg.entity);
+	if (entityIndex != -1)
+		this.entities[entityIndex] = msg.newentity;
+};
+
+/**
+ * If an entity is captured, or about to be killed (so its owner
+ * changes to '-1'), update the guards list
+ */
+Guard.prototype.OnGlobalOwnershipChanged = function(msg)
+{
+	// the ownership change may be on the guarded
+	if (this.entity == msg.entity)
+	{
+		var entities = this.GetEntities();
+		for each (var entity in entities)
+		{
+			if (msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, entity))
+			{
+				var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
+				if (cmpUnitAI && cmpUnitAI.IsGuardOf() && cmpUnitAI.IsGuardOf() == this.entity)
+					cmpUnitAI.RemoveGuard();
+				else
+					this.RemoveGuard(entity);
+			}
+		}
+		this.entities = entities;
+		return;
+	}
+
+	// or on some of its guards
+	if (this.entities.indexOf(msg.entity) != -1)
+	{
+		if (msg.to == -1)
+			this.RemoveGuard(msg.entity);
+		else if(!IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))
+		{
+			var cmpUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI);
+			if (cmpUnitAI)
+				cmpUnitAI.RemoveGuard();
+			else
+				this.RemoveGuard(msg.entity);
+		}
+	}
+};
+
+Engine.RegisterComponentType(IID_Guard, "Guard", Guard);
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js	(révision 14234)
+++ binaries/data/mods/public/simulation/components/GuiInterface.js	(copie de travail)
@@ -350,12 +350,22 @@
 			"state": cmpUnitAI.GetCurrentState(),
 			"orders": cmpUnitAI.GetOrders(),
 			"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
+			"canGuard": cmpUnitAI.CanGuard(),
+			"isGuarding": cmpUnitAI.IsGuardOf(),
 		};
 		// Add some information needed for ungarrisoning
 		if (cmpUnitAI.isGarrisoned && ret.player !== undefined)
 			ret.template = "p" + ret.player + "&" + ret.template;
 	}
 
+	var cmpGuard = Engine.QueryInterface(ent, IID_Guard);
+	if (cmpGuard)
+	{
+		ret.guard = {
+			"entities": cmpGuard.GetEntities(),
+		};
+	}
+	
 	var cmpGate = Engine.QueryInterface(ent, IID_Gate);
 	if (cmpGate)
 	{
@@ -783,6 +793,85 @@
 	}
 };
 
+GuiInterface.prototype.SetAdditionnalHighlight = function(player, cmd)
+{
+	var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+	var playerColours = {}; // cache of owner -> colour map
+
+	// remove the previous highlight when no more needed
+	for each (var ent in cmd.entitiesRemove)
+	{
+		var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
+		if (!cmpSelectable)
+			continue;
+
+		// Find the entity's owner's colour:
+		var owner = -1;
+		var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+		if (cmpOwnership)
+			owner = cmpOwnership.GetOwner();
+
+		var colour = playerColours[owner];
+		if (!colour)
+		{
+			var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
+			colour = cmpPlayer.GetColour();
+			playerColours[owner] = colour;
+		}
+
+		cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":0}, false);
+	}
+
+	// highlight the required units with player color
+	for each (var ent in cmd.entitiesColor)
+	{
+		var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
+		if (!cmpSelectable)
+			continue;
+
+		// Find the entity's owner's colour:
+		var owner = -1;
+		var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+		if (cmpOwnership)
+			owner = cmpOwnership.GetOwner();
+
+		// Only units from allies should be highligted
+		var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
+		if (!cmpPlayer || !cmpPlayer.IsMutualAlly(player))
+			continue;
+
+		var colour = playerColours[owner];
+		if (!colour)
+		{
+			colour = cmpPlayer.GetColour();
+			playerColours[owner] = colour;
+		}
+
+		cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":0.70}, true);
+	}
+
+	// highlight the required units in black
+	for each (var ent in cmd.entitiesBlack)
+	{
+		var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
+		if (!cmpSelectable)
+			continue;
+
+		// Find the entity's owner's colour:
+		var owner = -1;
+		var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+		if (cmpOwnership)
+			owner = cmpOwnership.GetOwner();
+
+		// Only units from allies should be highligted
+		var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
+		if (!cmpPlayer || !cmpPlayer.IsMutualAlly(player))
+			continue;
+
+		cmpSelectable.SetSelectionHighlight({"r": 0, "g": 0, "b": 0, "a":0.70}, true);
+	}
+};
+
 GuiInterface.prototype.SetStatusBars = function(player, cmd)
 {
 	for each (var ent in cmd.entities)
@@ -1801,6 +1890,7 @@
 	"IsStanceSelected": 1,
 
 	"SetSelectionHighlight": 1,
+	"SetAdditionnalHighlight": 1,
 	"SetStatusBars": 1,
 	"GetPlayerEntities": 1,
 	"DisplayRallyPoint": 1,
@@ -1812,6 +1902,7 @@
 	"GetTradingRouteGain": 1,
 	"GetTradingDetails": 1,
 	"CanAttack": 1,
+	"CanGuard": 1,
 	"GetBatchTime": 1,
 
 	"SetPathfinderDebugOverlay": 1,
Index: binaries/data/mods/public/simulation/components/Pack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Pack.js	(révision 14234)
+++ binaries/data/mods/public/simulation/components/Pack.js	(copie de travail)
@@ -162,8 +162,15 @@
 			if (cmpUnitAI.GetStanceName())
 				cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
 			cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
+			cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf());
 		}
 
+		// Maintain the list of guards
+		var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
+		var cmpNewGuard = Engine.QueryInterface(newEntity, IID_Guard);
+		if (cmpGuard && cmpNewGuard)
+			cmpNewGuard.SetEntities(cmpGuard.GetEntities());
+
 		Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: newEntity });
 
 		// Play notification sound
Index: binaries/data/mods/public/simulation/components/Promotion.js
===================================================================
--- binaries/data/mods/public/simulation/components/Promotion.js	(révision 14234)
+++ binaries/data/mods/public/simulation/components/Promotion.js	(copie de travail)
@@ -80,7 +80,13 @@
 	cmpPromotedUnitAI.AddOrders(orders);
 	var workOrders = cmpCurrentUnitAI.GetWorkOrders();
 	cmpPromotedUnitAI.SetWorkOrders(workOrders);
+	cmpPromotedUnitAI.SetGuardOf(cmpCurrentUnitAI.IsGuardOf());
 
+	var cmpCurrentUnitGuard = Engine.QueryInterface(this.entity, IID_Guard);
+	var cmpPromotedUnitGuard = Engine.QueryInterface(promotedUnitEntity, IID_Guard);
+	if (cmpCurrentUnitGuard && cmpPromotedUnitGuard)
+		cmpPromotedUnitGuard.SetEntities(cmpCurrentUnitGuard.GetEntities());
+
 	Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: promotedUnitEntity });
 
 	// Destroy current entity
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(révision 14234)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(copie de travail)
@@ -18,6 +18,9 @@
 	"<element name='FleeDistance'>" +
 		"<ref name='positiveDecimal'/>" +
 	"</element>" +
+	"<element name='CanGuard'>" +
+		"<data type='boolean'/>" +
+	"</element>" +
 	"<optional>" +
 		"<interleave>" +
 			"<element name='NaturalBehaviour' a:help='Behaviour of the unit in the absence of player commands (intended for animals)'>" +
@@ -164,6 +167,10 @@
 		// ignore
 	},
 
+	"GuardedAttacked": function(msg) {
+		// ignore
+	},
+
 	// Formation handlers:
 
 	"FormationLeave": function(msg) {
@@ -354,8 +361,21 @@
 		}
 	},
 
+	"Order.Guard": function(msg) {
+		if (!this.AddGuard(this.order.data.target))
+		{
+			this.FinishOrder();
+			return;
+		}
+
+		if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+			this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
+		else
+			this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
+	},
+
 	"Order.Flee": function(msg) {
-		// We use the distance between the enities to account for ranged attacks
+		// We use the distance between the entities to account for ranged attacks
 		var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
 		var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
 		if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1))
@@ -740,6 +760,12 @@
 				this.FinishOrder();
 		},
 
+		"Order.Guard": function(msg) {
+			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+			cmpFormation.CallMemberFunction("Guard", [msg.data.target, false]);
+			cmpFormation.Disband();
+		},
+
 		"Order.Stop": function(msg) {
 			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
 			cmpFormation.CallMemberFunction("Stop", [false]);
@@ -1259,12 +1285,56 @@
 			}
 		},
 
+		"GuardedAttacked": function(msg) {
+			// do nothing if we have a forced order in queue before the guard order
+			for (var i = 0; i < this.orderQueue.length; ++i)
+			{
+				if (this.orderQueue[i].type == "Guard")
+					break;
+				if (this.orderQueue[i].data && this.orderQueue[i].data.force)
+					return;
+			}
+			// if we already are targeting another unit still alive, finish with it first
+			if (this.order && this.order.type == "WalkAndFight")
+				if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker)) 
+					return;
+
+			var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+			var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
+			if (cmpIdentity && cmpIdentity.HasClass("Support") &&
+				cmpHealth && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
+			{
+				if (this.CanHeal(this.isGuardOf))
+					this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
+				else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
+					this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
+				return;
+			}
+
+			// target the unit
+			var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
+			if (!cmpPosition || !cmpPosition.IsInWorld())
+				return;
+			var pos = cmpPosition.GetPosition();
+			this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
+			// if we already had a WalkAndFight, keep only the most recent one in case the target has moved
+			if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
+				this.orderQueue.splice(1, 1);
+		},
+
 		"IDLE": {
 			"enter": function() {
 				// Switch back to idle animation to guarantee we won't
 				// get stuck with an incorrect animation
 				this.SelectAnimation("idle");
 
+				// If the unit is guarding/escorting, go back to its duty
+				if (this.isGuardOf)
+				{
+					this.Guard(this.isGuardOf, false);
+					return true;
+				}
+
 				// The GUI and AI want to know when a unit is idle, but we don't
 				// want to send frequent spurious messages if the unit's only
 				// idle for an instant and will quickly go off and do something else.
@@ -1366,6 +1436,116 @@
 			},
 		},
 
+		"GUARD": {
+			"enter": function () {
+			},
+
+			"RemoveGuard": function() {
+				this.FinishOrder();
+			},
+
+			"leave": function () {
+			},
+
+			"ESCORTING": {
+				"enter": function () {
+					// Show weapons rather than carried resources.
+					this.SetGathererAnimationOverride(true);
+
+					this.StartTimer(0, 1000);
+					this.SelectAnimation("move");
+					this.SetHeldPositionOnEntity(this.isGuardOf);
+					return false;
+				},
+
+				"Timer": function(msg) {
+					// Check the target is alive
+					if (!this.TargetIsAlive(this.isGuardOf))
+					{
+						this.StopMoving();
+						this.FinishOrder();
+						return;
+					}
+					this.SetHeldPositionOnEntity(this.isGuardOf);
+				},
+
+				"leave": function(msg) {
+					this.SetMoveSpeed(this.GetWalkSpeed());
+					this.StopTimer();
+				},
+
+				"MoveStarted": function(msg) {
+					// Adapt the speed to the one of the target if needed
+					var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+					if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange))
+					{
+						var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
+						if (cmpUnitAI)
+						{
+							var speed = cmpUnitAI.GetWalkSpeed();
+							if (speed < this.GetWalkSpeed())
+								this.SetMoveSpeed(speed);
+						}
+					}
+				},
+
+				"MoveCompleted": function() {
+					this.SetMoveSpeed(this.GetWalkSpeed());
+					if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+						this.SetNextState("GUARDING");
+				},
+			},
+
+			"GUARDING": {
+				"enter": function () {
+					this.StartTimer(1000, 1000);
+					this.SetHeldPositionOnEntity(this.entity);
+					this.SelectAnimation("idle");
+					return false;
+				},
+
+				"LosRangeUpdate": function(msg) {
+					// Start attacking one of the newly-seen enemy (if any)
+					if (this.GetStance().targetVisibleEnemies)
+						this.AttackEntitiesByPreference(msg.data.added);
+				},
+
+				"LosGaiaRangeUpdate": function(msg) {
+					// Start attacking one of the newly-seen enemy (if any)
+					if (this.GetStance().targetVisibleEnemies)
+						this.AttackGaiaEntitiesByPreference(msg.data.added);
+				},
+
+				"Timer": function(msg) {
+					// Check the target is alive
+					if (!this.TargetIsAlive(this.isGuardOf))
+					{
+						this.FinishOrder();
+						return;
+					}
+					// then check is the target has moved
+					if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) 
+						this.SetNextState("ESCORTING");
+					else
+					{
+						// if nothing better to do, check if the guarded needs to be healed or repaired
+						var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
+						if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()))
+						{
+							if (this.CanHeal(this.isGuardOf))
+								this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
+							else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
+								this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
+						}
+					}
+				},
+
+				"leave": function(msg) {
+					this.StopTimer();
+				},
+			},
+		},
+
 		"FLEEING": {
 			"enter": function() {
 				this.PlaySound("panic");
@@ -2270,6 +2450,12 @@
 					return false;
 				},
 
+				"LosRangeUpdate": function(msg) {
+				},
+
+				"LosGaiaRangeUpdate": function(msg) {
+				},
+
 				"leave": function() {
 					var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation);
 					if (cmpFoundation)
@@ -2715,6 +2901,8 @@
 	// Queue of remembered works
 	this.workOrders = [];
 
+	this.isGuardOf = undefined;
+
 	// For preventing increased action rate due to Stop orders or target death.
 	this.lastAttacked = undefined;
 	this.lastHealed = undefined;
@@ -3039,7 +3227,6 @@
 		error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
 	}
 
-	// Remove the order from the queue
 	this.orderQueue.shift();
 	this.order = this.orderQueue[0];
 
@@ -3391,6 +3578,9 @@
 	for each (var order in this.orderQueue)
 		if (order.data && order.data.target && order.data.target == msg.entity)
 			order.data.target = msg.newentity;
+
+	if (this.isGuardOf && this.isGuardOf == msg.entity)
+		this.isGuardOf = msg.newentity;
 };
 
 UnitAI.prototype.OnAttacked = function(msg)
@@ -3398,6 +3588,11 @@
 	UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
 };
 
+UnitAI.prototype.OnGuardedAttacked = function(msg)
+{
+	UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
+};
+
 UnitAI.prototype.OnHealthChanged = function(msg)
 {
 	UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
@@ -4043,6 +4238,19 @@
 	if (force)
 		return false;
 
+	// If we are guarding/escorting, don't abandon as long as the guarded is in target range of the attacker
+	if (this.isGuardOf)
+	{
+		var cmpUnitAI =  Engine.QueryInterface(target, IID_UnitAI);
+		var cmpAttack = Engine.QueryInterface(target, IID_Attack);
+		if (cmpUnitAI && cmpAttack)
+		{
+			for each (var type in cmpAttack.GetAttackTypes())
+				if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, IID_Attack, type))
+					return false;
+		}
+	}
+
 	// Stop if we're in hold-ground mode and it's too far from the holding point
 	if (this.GetStance().respondHoldGround)
 	{
@@ -4077,6 +4285,19 @@
 	if (this.GetStance().respondChase)
 		return true;
 
+	// If we are guarding/escorting, chase at least as long as the guarded is in target range of the attacker
+	if (this.isGuardOf)
+	{
+		var cmpUnitAI =  Engine.QueryInterface(target, IID_UnitAI);
+		var cmpAttack = Engine.QueryInterface(target, IID_Attack);
+		if (cmpUnitAI && cmpAttack)
+		{
+			for each (var type in cmpAttack.GetAttackTypes())
+				if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, IID_Attack, type))
+					return true;
+		}
+	}
+
 	if (force)
 		return true;
 
@@ -4177,6 +4398,7 @@
 
 		case "WalkToTarget":
 		case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
+		case "Guard":
 		case "Flee":
 		case "LeaveFoundation":
 		case "Attack":
@@ -4222,6 +4444,90 @@
 };
 
 /**
+ * Adds guard/escort order to the queue, forced by the player.
+ */
+UnitAI.prototype.Guard = function(target, queued)
+{
+	if (!this.CanGuard())
+	{
+		this.WalkToTarget(target, queued);
+		return;
+	}
+
+	// if we already had an old guard order, do nothing if the target is the same
+	// and the order is running, otherwise remove the previous order
+	if (this.isGuardOf)
+	{
+		if (this.isGuardOf == target && this.order && this.order.type == "Guard")
+			return;
+		else
+			this.RemoveGuard();
+	}
+
+	this.AddOrder("Guard", { "target": target, "force": false }, queued);
+};
+
+UnitAI.prototype.AddGuard = function(target)
+{
+	var cmpGuard = Engine.QueryInterface(target, IID_Guard);
+	if (!cmpGuard || !this.CanGuard())
+		return false;
+	this.isGuardOf = target;
+	this.guardRange = cmpGuard.GetRange(this.entity);
+	cmpGuard.AddGuard(this.entity);
+	return true;
+};
+
+UnitAI.prototype.RemoveGuard = function()
+{
+	if (this.isGuardOf)
+	{
+		var cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
+		if (cmpGuard)
+			cmpGuard.RemoveGuard(this.entity);
+		this.guardRange = undefined;
+		this.isGuardOf = undefined;
+	}
+
+	if (!this.order)
+		return;
+
+	if (this.order.type == "Guard")
+		UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
+	else
+		for (var i = 1; i < this.orderQueue.length; ++i)
+			if (this.orderQueue[i].type == "Guard")
+				this.orderQueue.splice(i, 1);
+};
+
+UnitAI.prototype.IsGuardOf = function()
+{
+	return this.isGuardOf;
+};
+
+UnitAI.prototype.SetGuardOf = function(entity)
+{
+	// entity may be undefined
+	this.isGuardOf = entity;
+};
+
+UnitAI.prototype.CanGuard = function()
+{
+	// Formation controllers should always respond to commands
+	// (then the individual units can make up their own minds)
+	if (this.IsFormationController())
+		return true;
+
+	// Do not let a unit already guarded to guard. This would work in principle,
+	// but would clutter the gui with too much buttons to take all cases into account
+	var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
+	if (cmpGuard && cmpGuard.GetEntities().length)
+		return false;
+
+	return (this.template.CanGuard == "true");
+};
+
+/**
  * Adds walk order to queue, forced by the player.
  */
 UnitAI.prototype.Walk = function(x, z, queued)
@@ -4682,6 +4988,15 @@
 	this.heldPosition = {"x": x, "z": z};
 };
 
+UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
+{
+	var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+	if (!cmpPosition || !cmpPosition.IsInWorld())
+		return;
+	var pos = cmpPosition.GetPosition();
+	this.SetHeldPosition(pos.x, pos.z);
+};
+
 UnitAI.prototype.GetHeldPosition = function(pos)
 {
 	return this.heldPosition;
Index: binaries/data/mods/public/simulation/components/interfaces/Guard.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/Guard.js	(révision 0)
+++ binaries/data/mods/public/simulation/components/interfaces/Guard.js	(copie de travail)
@@ -0,0 +1,5 @@
+Engine.RegisterInterface("Guard");
+
+// Message of the form { "guarded": entity, "data": msg },
+// sent whenever a guarded/escorted unit is attacked
+Engine.RegisterMessageType("GuardedAttacked");
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(révision 14234)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(copie de travail)
@@ -180,6 +180,15 @@
 		}
 		break;
 
+	case "remove-guard":
+		for each (var ent in entities)
+		{
+			var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+			if(cmpUnitAI)
+				cmpUnitAI.RemoveGuard();
+		}
+		break;
+
 	case "train":
 		// Check entity limits
 		var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
@@ -328,6 +337,20 @@
 		}
 		break;
 
+	case "guard":
+		// Verify that the target can be controlled by the player or is mutualAlly
+		if (CanControlUnitOrIsAlly(cmd.target, player, controlAllUnits))
+		{
+			GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
+				cmpUnitAI.Guard(cmd.target, cmd.queued);
+			});
+		}
+		else if (g_DebugCommands)
+		{
+			warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd));
+		}
+		break;
+
 	case "stop":
 		GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
 			cmpUnitAI.Stop(cmd.queued);
Index: binaries/data/mods/public/simulation/templates/special/formation.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/special/formation.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/special/formation.xml	(copie de travail)
@@ -17,6 +17,7 @@
     <DefaultStance>aggressive</DefaultStance>
     <FleeDistance>12.0</FleeDistance>
     <FormationController>true</FormationController>
+    <CanGuard>true</CanGuard>
   </UnitAI>
   <UnitMotion>
     <FormationController>true</FormationController>
Index: binaries/data/mods/public/simulation/templates/template_structure.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_structure.xml	(copie de travail)
@@ -15,6 +15,7 @@
     <GarrisonArrowMultiplier>0</GarrisonArrowMultiplier>
     <GarrisonArrowClasses>Ranged Infantry</GarrisonArrowClasses>
   </BuildingAI>
+  <Guard/>
   <BuildRestrictions>
     <PlacementType>land</PlacementType>
     <Territory>own</Territory>
Index: binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_unit.xml	(copie de travail)
@@ -102,7 +102,9 @@
     <DefaultStance>aggressive</DefaultStance>
     <FleeDistance>12.0</FleeDistance>
     <FormationController>false</FormationController>
+    <CanGuard>true</CanGuard>
   </UnitAI>
+  <Guard/>
   <UnitMotion>
     <FormationController>false</FormationController>
     <WalkSpeed>7.5</WalkSpeed>
Index: binaries/data/mods/public/simulation/templates/template_unit_fauna.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_fauna.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_unit_fauna.xml	(copie de travail)
@@ -22,6 +22,7 @@
   </Minimap>
   <ResourceGatherer disable=""/>
   <UnitAI>
+    <CanGuard>false</CanGuard>
     <RoamDistance>8.0</RoamDistance>
     <FleeDistance>24.0</FleeDistance>
     <RoamTimeMin>2000</RoamTimeMin>
@@ -29,6 +30,7 @@
     <FeedTimeMin>15000</FeedTimeMin>
     <FeedTimeMax>60000</FeedTimeMax>
   </UnitAI>
+  <Guard disable=""/>
   <UnitMotion>
     <WalkSpeed>5.0</WalkSpeed>
     <Run>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml	(copie de travail)
@@ -50,6 +50,9 @@
       <order_attack>actor/ship/boat_move.xml</order_attack>
     </SoundGroups>
   </Sound>
+  <UnitAI>
+    <CanGuard>false</CanGuard>
+  </UnitAI>
   <UnitMotion>
     <WalkSpeed>8.5</WalkSpeed>
   </UnitMotion>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_merchant.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_merchant.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_merchant.xml	(copie de travail)
@@ -47,6 +47,9 @@
     <MaxDistance>10.0</MaxDistance>
     <GainMultiplier>1.0</GainMultiplier>
   </Trader>
+  <UnitAI>
+    <CanGuard>false</CanGuard>
+  </UnitAI>
   <UnitMotion>
     <WalkSpeed>10.5</WalkSpeed>
   </UnitMotion>
Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml	(copie de travail)
@@ -13,6 +13,9 @@
     <GenericName>Siege</GenericName>
     <RequiredTechnology>phase_city</RequiredTechnology>
   </Identity>
+  <UnitAI>
+    <CanGuard>false</CanGuard>
+  </UnitAI>
   <Position>
     <Anchor>pitch-roll</Anchor>
   </Position>
Index: binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml	(révision 14234)
+++ binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml	(copie de travail)
@@ -40,6 +40,9 @@
     <MaxDistance>2.0</MaxDistance>
     <GainMultiplier>1.0</GainMultiplier>
   </Trader>
+  <UnitAI>
+    <CanGuard>false</CanGuard>
+  </UnitAI>
   <UnitMotion>
     <WalkSpeed>8.0</WalkSpeed>
   </UnitMotion>
