Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(révision 14662)
+++ binaries/data/mods/public/gui/session/input.js	(copie de travail)
@@ -1443,7 +1443,7 @@
 		return true;
 
 	case "setup-trade-route":
-		Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target});
+		Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target, "source": undefined, "route": undefined, "queued": queued});
 		Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] });
 		return true;
 x
Index: binaries/data/mods/public/simulation/components/Trader.js
===================================================================
--- binaries/data/mods/public/simulation/components/Trader.js	(révision 14662)
+++ binaries/data/mods/public/simulation/components/Trader.js	(copie de travail)
@@ -247,8 +247,10 @@
 {
 	if (this.goods.amount && this.goods.origin == this.firstMarket)
 		return this.secondMarket;
-	else
-		return this.firstMarket;
+
+	if (this.goods.amount && this.goods.origin != this.secondMarket)
+		this.goods.amount = null;   // leftover from previous trading
+	return this.firstMarket;
 };
 
 Trader.prototype.StopTrading = function()
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(révision 14662)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(copie de travail)
@@ -641,15 +641,19 @@
 
 		var nextMarket = cmpTrader.GetNextMarket();
 		if (nextMarket == this.order.data.firstMarket)
-			var state = "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET";
+			var state = "TRADE.APPROACHINGFIRSTMARKET";
 		else
-			var state = "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET";
+			var state = "TRADE.APPROACHINGSECONDMARKET";
 
+		// TODO find the nearest way-point from our position, and start with it
+		this.waypoints = undefined;
 		if (this.MoveToMarket(nextMarket))
 		{
 			// We've started walking to the next market
 			this.SetNextState(state);
 		}
+		else
+			this.FinishOrder();
 	},
 
 	"Order.Repair": function(msg) {
@@ -775,6 +779,13 @@
 				this.FinishOrder();
 		},
 
+		"Order.WalkToTarget": function(msg) {
+			if (this.MoveToTarget(this.order.data.target))
+				this.SetNextState("WALKING");
+			else
+				this.FinishOrder();
+		},
+
 		"Order.WalkToPointRange": function(msg) {
 			if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max))
 				this.SetNextState("WALKING");
@@ -2486,7 +2497,10 @@
 				},
 
 				"MoveCompleted": function() {
-					this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET");
+					if (this.waypoints && this.waypoints.length)
+						this.MoveToMarket(this.order.data.firstMarket);
+					else
+						this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "APPROACHINGSECONDMARKET");
 				},
 			},
 
@@ -2496,8 +2510,10 @@
 				},
 
 				"MoveCompleted": function() {
-					this.order.data.firstPass = false;
-					this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
+					if (this.waypoints && this.waypoints.length)
+						this.MoveToMarket(this.order.data.secondMarket);
+					else
+						this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "APPROACHINGFIRSTMARKET");
 				},
 			},
 		},
@@ -3438,9 +3454,7 @@
 		// If the order was rejected then immediately take it off
 		// and process the remaining queue
 		if (ret && ret.discardOrder)
-		{
 			this.FinishOrder();
-		}
 	}
 };
 
@@ -4658,6 +4672,9 @@
 
 UnitAI.prototype.AddOrder = function(type, data, queued)
 {
+	if (this.expectedRoute)
+		this.expectedRoute = undefined;
+
 	if (queued)
 		this.PushOrder(type, data);
 	else
@@ -4762,7 +4779,10 @@
  */
 UnitAI.prototype.Walk = function(x, z, queued)
 {
-	this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
+	if (this.expectedRoute && queued)
+		this.expectedRoute.push({ "x": x, "z": z });
+	else
+		this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
 };
 
 /**
@@ -4945,8 +4965,10 @@
 /**
  * Adds trade order to the queue. Either walk to the first market, or
  * start a new route. Not forced, so it can be interrupted by attacks.
+ * The possible route may be given directly as a SetupTradeRoute argument
+ * if coming from a RallyPoint, or through this.expectedRoute if a user command.
  */
-UnitAI.prototype.SetupTradeRoute = function(target, source, queued)
+UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued)
 {
 	if (!this.CanTrade(target))
 	{
@@ -4954,32 +4976,71 @@
 		return;
 	}
 
-	var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
-	var marketsChanged = cmpTrader.SetTargetMarket(target, source);
+	var marketsChanged = this.SetTargetMarket(target, source);
 	if (marketsChanged)
 	{
+		var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
 		if (cmpTrader.HasBothMarkets())
-			this.AddOrder("Trade", { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "force": false }, queued);
+		{
+			if (!route && this.expectedRoute && this.expectedRoute.length)
+			{
+				route = this.expectedRoute.slice();
+				this.expectedRoute = undefined;
+			}
+
+			var data = { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "route": route, "force": false };
+			if (this.IsFormationController())
+				this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
+			else
+				this.AddOrder("Trade", data, queued);
+		}
 		else
-			this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
+		{
+			if (this.IsFormationController())
+				this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]);
+			else
+				this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
+			this.expectedRoute = [];
+		}
 	}
 };
 
+UnitAI.prototype.SetTargetMarket = function(target, source)
+{
+	var  cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
+	if (!cmpTrader)
+		return false;
+	var marketsChanged = cmpTrader.SetTargetMarket(target, source);
+
+	if (this.IsFormationController())
+		this.CallMemberFunction("SetTargetMarket", [target, source]);
+
+	return marketsChanged;
+};
+
 UnitAI.prototype.MoveToMarket = function(targetMarket)
 {
-	if (this.MoveToTarget(targetMarket))
+	if (this.waypoints && this.waypoints.length > 1)
 	{
-		// We've started walking to the market
-		return true;
+		var point = this.waypoints.pop();
+		var ok = this.MoveToPoint(point.x, point.z);
+		if (!ok)
+			ok = this.MoveToMarket(targetMarket);
 	}
 	else
 	{
-		// We can't reach the market.
-		// Give up.
+		this.waypoints = undefined;
+		var ok = this.MoveToTarget(targetMarket);
+	}
+
+	if (!ok)
+	{
+		// We can't reach the market. Give up.
 		this.StopMoving();
 		this.StopTrading();
-		return false;
 	}
+
+	return ok;
 };
 
 UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName)
@@ -4999,16 +5060,24 @@
 			this.StopTrading();
 			return;
 		}
-		if (this.MoveToMarket(nextMarket))
+
+		if (this.order.data.route && this.order.data.route.length)
 		{
-			// We've started walking to the next market
+			this.waypoints = this.order.data.route.slice();
+			if (nextFsmStateName == "APPROACHINGSECONDMARKET")
+				this.waypoints.reverse();
+			this.waypoints.unshift(null);  // additionnal dummy point for the market
+		}
+
+		if (this.MoveToMarket(nextMarket))	// We've started walking to the next market
 			this.SetNextState(nextFsmStateName);
-		}
+		else
+			this.StopTrading();
 	}
 	else
 	{
-		// If the current market is not reached try again
-		this.MoveToMarket(currentMarket);
+		if (!this.MoveToMarket(currentMarket))	// If the current market is not reached try again
+			this.StopTrading();
 	}
 };
 
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(révision 14662)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(copie de travail)
@@ -489,13 +489,9 @@
 		break;
 
 	case "setup-trade-route":
-		for each (var ent in entities)
-		{
-			var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
-			if (cmpUnitAI)
-				cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source);
-		}
-		break;
+		GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
+			cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
+		});
 
 	case "select-required-goods":
 		for each (var ent in entities)
Index: binaries/data/mods/public/simulation/helpers/RallyPointCommands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/RallyPointCommands.js	(révision 14662)
+++ binaries/data/mods/public/simulation/helpers/RallyPointCommands.js	(copie de travail)
@@ -89,6 +89,30 @@
 			break;
 		}
 	}
+
+	// special case: trade route with waypoints
+	// (we do not modify the RallyPoint before, as we want it to be displayed with all way-points)
+	if (ret.length > 1 && ret[ret.length-1].type == "setup-trade-route")
+	{
+
+		var route = [];
+		var waypoints = ret.length - 1;
+		for (var i = 0; i < waypoints; ++i)
+		{
+			if (ret[i].type != "walk")
+			{
+				route = undefined;
+				break;
+			}
+			route.push( {"x": ret[i].x, "z": ret[i].z} );
+		}
+		if (route && route.length > 0)
+		{
+			ret.splice(0, waypoints);
+			ret[0].route = route;
+		}
+	}
+
 	return ret;
 }
 
Index: binaries/data/mods/public/simulation/templates/template_formation.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_formation.xml	(révision 14662)
+++ binaries/data/mods/public/simulation/templates/template_formation.xml	(copie de travail)
@@ -28,7 +28,7 @@
     <TurnRate>3.0</TurnRate>
   </Position>
   <UnitAI>
-	<AlertReactiveLevel>2</AlertReactiveLevel>
+    <AlertReactiveLevel>2</AlertReactiveLevel>
     <DefaultStance>aggressive</DefaultStance>
     <FleeDistance>12.0</FleeDistance>
     <FormationController>true</FormationController>
@@ -40,4 +40,8 @@
     <PassabilityClass>default</PassabilityClass>
     <CostClass>default</CostClass>
   </UnitMotion>
+  <Trader>
+    <MaxDistance>5.0</MaxDistance>
+    <GainMultiplier>1.0</GainMultiplier>
+  </Trader>
 </Entity>
