Index: source/gui/CGUI.cpp
===================================================================
--- source/gui/CGUI.cpp	(revision 11026)
+++ source/gui/CGUI.cpp	(working copy)
@@ -438,6 +438,7 @@
 	AddObjectType("input",			&CInput::ConstructObject);
 	AddObjectType("list",			&CList::ConstructObject);
 	AddObjectType("dropdown",		&CDropDown::ConstructObject);
+	AddObjectType("tooltip",		&CTooltip::ConstructObject);
 }
 
 void CGUI::Draw()
Index: source/gui/CTooltip.cpp
===================================================================
--- source/gui/CTooltip.cpp	(revision 11026)
+++ source/gui/CTooltip.cpp	(working copy)
@@ -135,6 +135,9 @@
 
 void CTooltip::HandleMessage(SGUIMessage &Message)
 {
+	if (Message.type == GUIM_MOUSE_MOTION)
+		if (GUI<CPos>::SetSetting(this, "_mousepos", GetMousePos()) != PSRETURN_OK)
+			debug_warn(L"Failed to set tooltip mouse position");
 	IGUITextOwner::HandleMessage(Message);
 }
 
Index: binaries/data/mods/public/gui/session/session.xml
===================================================================
--- binaries/data/mods/public/gui/session/session.xml	(revision 11026)
+++ binaries/data/mods/public/gui/session/session.xml	(working copy)
@@ -458,6 +458,11 @@
 		</object>
 
 		<!-- ================================  ================================ -->
+		<!-- Information tooltip -->
+		<!-- ================================  ================================ -->
+		<object name="informationTooltip" type="tooltip" style="informationTooltip" />
+
+		<!-- ================================  ================================ -->
 		<!-- START of BOTTOM PANEL -->
 		<!-- ================================  ================================ -->
 
@@ -596,7 +601,6 @@
 						<!-- Resource carrying icon/counter -->
 						<object size="0 40 48 88" type="image" name="resourceCarryingIcon" style="resourceIcon"/>
 						<object size="0 80 48 100" type="text" name="resourceCarryingText" style="statsText"/>
-
 					</object>
 
 					<!-- Big unit icon -->
@@ -751,6 +755,18 @@
 					</object>
 				</object>
 
+				<object name="unitTradingPanel"
+					size="14 12 100% 100%"
+				>
+					<object size="0 0 100% 100%">
+						<repeat count="4">
+							<object name="unitTradingButton[n]" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottom">
+								<object name="unitTradingIcon[n]" type="image" ghost="true" size="3 3 43 43"/>
+							</object>
+						</repeat>
+					</object>
+				</object>
+
 				<object name="unitQueuePanel"
 					size="4 -56 100% 0"
 					type="image"
Index: binaries/data/mods/public/gui/session/styles.xml
===================================================================
--- binaries/data/mods/public/gui/session/styles.xml	(revision 11026)
+++ binaries/data/mods/public/gui/session/styles.xml	(working copy)
@@ -161,6 +161,16 @@
 		tooltip_style="snToolTip"
 	/>
 
+	<style name="informationTooltip"
+		anchor="top"
+		buffer_zone="4"
+		font="serif-bold-14"
+		maxwidth="300"
+		offset="16 32"
+		sprite="BackgroundInformationTooltip"
+		textcolor="255 255 255"
+	/>
+
 	<!-- ================================  ================================ -->
 	<!-- Misc Styles -->
 	<!-- ================================  ================================ -->
Index: binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_details.js	(revision 11026)
+++ binaries/data/mods/public/gui/session/selection_details.js	(working copy)
@@ -135,6 +135,14 @@
 	//		getGUIObjectByName("resourceCarryingText").hidden = true;
 	//	}
 	}
+	// Use the same indicators for traders
+	else if (entState.trader && entState.trader.goods.amount > 0)
+	{
+		getGUIObjectByName("resourceCarryingIcon").hidden = false;
+		getGUIObjectByName("resourceCarryingText").hidden = false;
+		getGUIObjectByName("resourceCarryingIcon").cell_id = RESOURCE_ICON_CELL_IDS[entState.trader.goods.type];
+		getGUIObjectByName("resourceCarryingText").caption = entState.trader.goods.amount;
+	}
 	else
 	{
 		getGUIObjectByName("resourceCarryingIcon").hidden = true;
Index: binaries/data/mods/public/gui/session/utility_functions.js
===================================================================
--- binaries/data/mods/public/gui/session/utility_functions.js	(revision 11026)
+++ binaries/data/mods/public/gui/session/utility_functions.js	(working copy)
@@ -83,6 +83,17 @@
 			player.offline = true;
 }
 
+function hasClass(entState, className)
+{
+	if (entState.identity)
+	{
+		var classes = entState.identity.classes;
+		if (classes && classes.length)
+			return (classes.indexOf(className) != -1);
+	}
+	return false;
+}
+
 function isUnit(entState)
 {
 	if (entState.identity)
Index: binaries/data/mods/public/gui/session/input.js
===================================================================
--- binaries/data/mods/public/gui/session/input.js	(revision 11026)
+++ binaries/data/mods/public/gui/session/input.js	(working copy)
@@ -52,8 +52,11 @@
 var doublePressTimer = 0;
 var prevHotkey = 0;
 
-function updateCursor()
+function updateCursorAndTooltip()
 {
+	var cursorSet = false;
+	var tooltipSet = false;
+	var informationTooltip = getGUIObjectByName("informationTooltip");
 	if (!mouseIsOverObject)
 	{
 		var action = determineAction(mouseX, mouseY);
@@ -64,13 +67,22 @@
 				if (action.cursor)
 				{
 					Engine.SetCursor(action.cursor);
-					return;
+					cursorSet = true;
 				}
+				if (action.tooltip)
+				{
+					tooltipSet = true;
+					informationTooltip.caption = action.tooltip;
+					informationTooltip.hidden = false;
+				}
 			}
 		}
 	}
 
-	Engine.SetCursor("arrow-default");
+	if (!cursorSet)
+		Engine.SetCursor("arrow-default");
+	if (!tooltipSet)
+		informationTooltip.hidden = true;
 }
 
 function updateBuildingPlacementPreview()
@@ -255,6 +267,36 @@
 				}
 			}
 			break;
+		case "setup-trade-route":
+			// If ground or sea trade possible
+			if ((entState.trader && hasClass(entState, "Organic") && (playerOwned || allyOwned) && hasClass(targetState, "Market")) ||
+				(entState.trader && hasClass(entState, "Ship") && (playerOwned || allyOwned) && hasClass(targetState, "SeaMarket")))
+			{
+				var tradingData = {"trader": entState.id, "target": target};
+				var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData);
+				var tooltip;
+				switch (tradingDetails.type)
+				{
+				case "is first":
+					tooltip = "First trade market.";
+					if (tradingDetails.hasBothMarkets)
+						tooltip += " Click to establish another route."
+					else
+						tooltip += " Click on another market to establish a trade route."
+					break;
+				case "is second":
+					tooltip = "Second trade market. Click to establish another route.";
+					break;
+				case "set first":
+					tooltip = "Set as first trade market";
+					break;
+				case "set second":
+					tooltip = "Set as second trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + ".";
+					break;
+				}
+				return {"possible": true, "tooltip": tooltip};
+			}
+			break;
 		case "gather":
 			if (targetState.resourceSupply && (playerOwned || gaiaOwned))
 			{
@@ -278,6 +320,7 @@
 		case "attack":
 			if (entState.attack && targetState.hitpoints && enemyOwned)
 				return {"possible": true};
+			break;
 		}
 	}
 	if (action == "move")
@@ -362,7 +405,9 @@
 	else
 	{
 		var actionInfo = undefined;
-		if ((actionInfo = getActionInfo("gather", target)).possible)
+		if ((actionInfo = getActionInfo("setup-trade-route", target)).possible)
+			return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target};
+		else if ((actionInfo = getActionInfo("gather", target)).possible)
 			return {"type": "gather", "cursor": actionInfo.cursor, "target": target};
 		else if ((actionInfo = getActionInfo("returnresource", target)).possible)
 			return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target};
@@ -962,7 +1007,6 @@
 				updateBuildingPlacementPreview();
 				break;
 			}
-
 			break;
 
 		}
@@ -1008,6 +1052,10 @@
 		Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
 		return true;
 
+	case "setup-trade-route":
+		Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target});
+		return true;
+
 	case "garrison":
 		Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued});
 		// TODO: Play a sound?
@@ -1100,13 +1148,18 @@
 	inputState = INPUT_BUILDING_PLACEMENT;
 }
 
+// Called by GUI when user changes preferred trading goods
+function selectTradingPreferredGoods(data)
+{
+	Engine.PostNetworkCommand({"type": "select-trading-goods", "trader": data.trader, "preferredGoods": data.preferredGoods });
+}
+
 // Called by GUI when user clicks exchange resources button
 function exchangeResources(command)
 {
 	Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount});
 }
 
-
 // Batch training:
 // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
 // When the user releases shift, or clicks on a different training button, we create the batched units
Index: binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- binaries/data/mods/public/gui/session/unit_commands.js	(revision 11026)
+++ binaries/data/mods/public/gui/session/unit_commands.js	(working copy)
@@ -13,6 +13,9 @@
 const UNIT_PANEL_BASE = -52; // QUEUE: The offset above the main panel (will often be negative)
 const UNIT_PANEL_HEIGHT = 44; // QUEUE: The height needed for a row of buttons
 
+// Trading constants
+const TRADING_RESOURCES = ["food", "wood", "stone", "metal"];
+
 // Barter constants
 const BARTER_RESOURCE_AMOUNT_TO_SELL = 100;
 const BARTER_BUNCH_MULTIPLIER = 5;
@@ -20,10 +23,10 @@
 const BARTER_ACTIONS = ["Sell", "Buy"];
 
 // The number of currently visible buttons (used to optimise showing/hiding)
-var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Barter": 0, "Training": 0, "Construction": 0, "Command": 0, "Stance": 0};
+var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Barter": 0, "Trading": 0, "Construction": 0, "Command": 0, "Stance": 0};
 
 // Unit panels are panels with row(s) of buttons
-var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Barter", "Training", "Construction", "Research", "Stance", "Command"];
+var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Trading", "Construction", "Research", "Stance", "Command"];
 
 // Indexes of resources to sell and buy on barter panel
 var g_barterSell = 0;
@@ -132,6 +135,7 @@
 function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
 {
 	usedPanels[guiName] = 1;
+
 	var numberOfItems = items.length;
 	var selection = g_Selection.toList();
 	var garrisonGroups = new EntityGroups();
@@ -389,6 +393,25 @@
 	g_unitPanelButtons[guiName] = numButtons;
 }
 
+// Sets up "unit trading panel" - special case for setupUnitPanel
+function setupUnitTradingPanel(unitEntState)
+{
+	for (var i = 0; i < TRADING_RESOURCES.length; i++)
+	{
+		var resource = TRADING_RESOURCES[i];
+		var button = getGUIObjectByName("unitTradingButton["+i+"]");
+		button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46";
+		var selectTradingPreferredGoodsData = { "trader": unitEntState.id, "preferredGoods": resource };
+		button.onpress = (function(e){ return function() { selectTradingPreferredGoods(e); } })(selectTradingPreferredGoodsData);
+		button.enabled = true;
+		button.tooltip = "Set " + resource + " as trading goods";
+		var icon = getGUIObjectByName("unitTradingIcon["+i+"]");
+		var preferredGoods = unitEntState.trader.preferredGoods;
+		var imageNameSuffix = (resource == preferredGoods) ? "selected" : "inactive";
+		icon.sprite = "stretched:session/resources/" + resource + "_" + imageNameSuffix + ".png";
+	}
+}
+
 // Sets up "unit barter panel" - special case for setupUnitPanel
 function setupUnitBarterPanel(unitEntState)
 {
@@ -528,6 +551,12 @@
 			setupUnitPanel("Queue", usedPanels, entState, entState.training.queue,
 				function (item) { removeFromTrainingQueue(entState.id, item.id); } );
 
+		if (entState.trader)
+		{
+			usedPanels["Trading"] = 1;
+			setupUnitTradingPanel(entState);
+		}
+
 //		supplementalDetailsPanel.hidden = false;
 //		commandsPanel.hidden = isInvisible;
 	}
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js	(revision 11026)
+++ binaries/data/mods/public/gui/session/session.js	(working copy)
@@ -185,7 +185,7 @@
 		handleNetMessage(message);
 	}
 
-	updateCursor();
+	updateCursorAndTooltip();
 
 	// If the selection changed, we need to regenerate the sim display
 	if (g_Selection.dirty)
Index: binaries/data/mods/public/gui/common/common_sprites.xml
===================================================================
--- binaries/data/mods/public/gui/common/common_sprites.xml	(revision 11026)
+++ binaries/data/mods/public/gui/common/common_sprites.xml	(working copy)
@@ -70,6 +70,14 @@
 		/>
 	</sprite>
 
+	<sprite name="BackgroundInformationTooltip">
+		<image
+			backcolor="0 0 0 191"
+			size="0 0 100% 100%"
+			border="false"
+		/>
+	</sprite>
+
 	<sprite name="BackgroundIndentFillDark">
 		<!-- Starting with top left corner continuing in a clockwise manner -->
 		<!-- Top border -->
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js	(revision 11026)
+++ binaries/data/mods/public/simulation/helpers/Commands.js	(working copy)
@@ -404,6 +404,20 @@
 		}
 		break;
 
+	case "setup-trade-route":
+		for each (var ent in cmd.entities)
+		{
+			var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
+			if (cmpUnitAI)
+				cmpUnitAI.SetupTradeRoute(cmd.target);
+		}
+		break;
+
+	case "select-trading-goods":
+		var cmpTrader = Engine.QueryInterface(cmd.trader, IID_Trader);
+		cmpTrader.SetPreferredGoods(cmd.preferredGoods);
+		break;
+
 	case "barter":
 		var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
 		cmpBarter.ExchangeResources(playerEnt, cmd.sell, cmd.buy, cmd.amount);
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js	(revision 11026)
+++ binaries/data/mods/public/simulation/components/GuiInterface.js	(working copy)
@@ -185,6 +185,15 @@
 		};
 	}
 
+	var cmpTrader = Engine.QueryInterface(ent, IID_Trader);
+	if (cmpTrader)
+	{
+		ret.trader = {
+			"goods": cmpTrader.GetGoods(),
+			"preferredGoods": cmpTrader.GetPreferredGoods()
+		};
+	}
+
 	var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
 	if (cmpFoundation)
 	{
@@ -680,6 +689,42 @@
 	return 0;
 };
 
+GuiInterface.prototype.GetTradingDetails = function(player, data)
+{
+	var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
+	var cmpEntityIdentity = Engine.QueryInterface(data.trader, IID_Identity);
+	var cmpEntityPlayer = QueryOwnerInterface(data.trader, IID_Player);
+	var entityPlayerId = cmpEntityPlayer.GetPlayerID();
+	var cmpTargetIdentity = Engine.QueryInterface(data.target, IID_Identity);
+	var cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
+	var targetPlayerId = cmpTargetPlayer.GetPlayerID();
+
+	var ownershipSuitableForTrading = (entityPlayerId == targetPlayerId) || cmpEntityPlayer.IsAlly(targetPlayerId);
+	var landTradingPossible = cmpEntityIdentity.HasClass("Organic") && cmpTargetIdentity.HasClass("Market");
+	var seaTradingPossible = cmpEntityIdentity.HasClass("Ship") && cmpTargetIdentity.HasClass("SeaMarket");
+	if (!(ownershipSuitableForTrading && cmpEntityTrader && (landTradingPossible || seaTradingPossible)))
+		return null;
+	var firstMarket = cmpEntityTrader.GetFirstMarket();
+	var secondMarket = cmpEntityTrader.GetSecondMarket();
+	var result = null;
+	if (data.target == firstMarket)
+		result = {"type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() };
+	else if (data.target == secondMarket)
+		result = {"type": "is second"};
+	else if (firstMarket == null)
+		result = {"type": "set first"};
+	else if (secondMarket == null)
+		result = {
+			"type": "set second",
+			"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
+			"goods": cmpEntityTrader.GetPreferredGoods()
+		};
+	// Else both markets are not null and target is different from them
+	else
+		result = {"type": "set first"};
+	return result;
+};
+
 GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
 {
 	var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
@@ -738,6 +783,7 @@
 	"GetFoundationSnapData": 1,
 	"PlaySound": 1,
 	"FindIdleUnit": 1,
+	"GetTradingDetails": 1,
 
 	"SetPathfinderDebugOverlay": 1,
 	"SetObstructionDebugOverlay": 1,
Index: binaries/data/mods/public/simulation/components/interfaces/Trader.js
===================================================================
--- binaries/data/mods/public/simulation/components/interfaces/Trader.js	(revision 0)
+++ binaries/data/mods/public/simulation/components/interfaces/Trader.js	(revision 0)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Trader");
Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js	(revision 11026)
+++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js	(working copy)
@@ -12,6 +12,7 @@
 Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
 Engine.LoadComponentScript("interfaces/ResourceSupply.js");
 Engine.LoadComponentScript("interfaces/TrainingQueue.js");
+Engine.LoadComponentScript("interfaces/Trader.js")
 Engine.LoadComponentScript("interfaces/Timer.js");
 Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
 Engine.LoadComponentScript("interfaces/UnitAI.js");
Index: binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- binaries/data/mods/public/simulation/components/Identity.js	(revision 11026)
+++ binaries/data/mods/public/simulation/components/Identity.js	(working copy)
@@ -103,6 +103,8 @@
 						"<value>Slave</value>" +
 						"<value>CitizenSoldier</value>" +
 						"<value>Trade</value>" +
+						"<value>Market</value>" +
+						"<value>SeaMarket</value>" +
 						"<value>Warship</value>" +
 						"<value>SeaCreature</value>" +
 						"<value>ForestPlant</value>" +
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js	(revision 11026)
+++ binaries/data/mods/public/simulation/components/UnitAI.js	(working copy)
@@ -341,7 +341,15 @@
 			return;
 		}
 	},
-	
+
+	"Order.Trade": function(msg) {
+		if (this.MoveToMarket(this.order.data.firstMarket))
+		{
+			// We've started walking to the first market
+			this.SetNextState("INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
+		}
+	},
+
 	"Order.Repair": function(msg) {
 		// Try to move within range
 		if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
@@ -1048,6 +1056,34 @@
 			},
 		},
 
+		"TRADE": {
+			"Attacked": function(msg) {
+				// Ignore attack
+				// TODO: Inform player
+			},
+
+			"APPROACHINGFIRSTMARKET": {
+				"enter": function () {
+					this.SelectAnimation("move");
+				},
+
+				"MoveCompleted": function() {
+					this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET");
+				},
+			},
+
+			"APPROACHINGSECONDMARKET": {
+				"enter": function () {
+					this.SelectAnimation("move");
+				},
+
+				"MoveCompleted": function() {
+					this.order.data.firstPass = false;
+					this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
+				},
+			},
+		},
+
 		"REPAIR": {
 			"APPROACHING": {
 				"enter": function () {
@@ -2364,6 +2400,70 @@
 	this.AddOrder("ReturnResource", { "target": target }, queued);
 };
 
+UnitAI.prototype.SetupTradeRoute = function(target, queued)
+{
+	if (!this.CanTrade(target))
+	{
+		this.WalkToTarget(target, queued);
+		return;
+	}
+
+	var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
+	var marketsChanged = cmpTrader.SetTargetMarket(target);
+	if (marketsChanged)
+	{
+		if (cmpTrader.HasBothMarkets())
+			this.AddOrder("Trade", { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket() }, queued);
+		else
+			this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
+	}
+}
+
+UnitAI.prototype.MoveToMarket = function(targetMarket)
+{
+	if (this.MoveToTarget(targetMarket))
+	{
+		// We've started walking to the market
+		return true;
+	}
+	else
+	{
+		// We can't reach the market.
+		// Give up.
+		this.StopMoving();
+		this.FinishOrder();
+		var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
+		cmpTrader.DropGoods();
+		return false;
+	}
+}
+
+UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName)
+{
+	if (this.CheckTargetRange(currentMarket, IID_Trader))
+	{
+		this.PerformTrade();
+		if (this.MoveToMarket(nextMarket))
+		{
+			// We've started walking to the next market
+			this.SetNextState(nextFsmStateName);
+		}
+	}
+	else
+	{
+		// Market don't reached, give up
+		this.FinishOrder();
+		var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
+		cmpTrader.DropGoods();
+	}
+}
+
+UnitAI.prototype.PerformTrade = function()
+{
+	var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
+	cmpTrader.PerformTrade();
+}
+
 UnitAI.prototype.Repair = function(target, autocontinue, queued)
 {
 	if (!this.CanRepair(target))
@@ -2583,6 +2683,21 @@
 	return true;
 };
 
+UnitAI.prototype.CanTrade = function(target)
+{
+	// Formation controllers should always respond to commands
+	// (then the individual units can make up their own minds)
+	if (this.IsFormationController())
+		return true;
+
+	// Verify that we're able to respond to Trade commands
+	var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
+	if (!cmpTrader)
+		return false;
+
+	return true;
+}
+
 UnitAI.prototype.CanRepair = function(target)
 {
 	// Formation controllers should always respond to commands
Index: binaries/data/mods/public/simulation/components/Looter.js
===================================================================
--- binaries/data/mods/public/simulation/components/Looter.js	(revision 11026)
+++ binaries/data/mods/public/simulation/components/Looter.js	(working copy)
@@ -21,6 +21,20 @@
 	}
 	var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
 	cmpPlayer.AddResources(cmpLoot.GetResources());
+
+	// If target entity has trader component, add carried goods to loot too
+	var cmpTrader = Engine.QueryInterface(targetEntity, IID_Trader);
+	if (cmpTrader)
+	{
+		var carriedGoods = cmpTrader.GetGoods();
+		if (carriedGoods.amount > 0)
+		{
+			// Convert from {type:<type>,amount:<amount>} to {<type>:<amount>}
+			var resourcesToAdd = {};
+			resourcesToAdd[carriedGoods.type] = carriedGoods.amount;
+			cmpPlayer.AddResources(resourcesToAdd);
+		}
+	}
 }
 
 Engine.RegisterComponentType(IID_Looter, "Looter", Looter);
Index: binaries/data/mods/public/simulation/components/Trader.js
===================================================================
--- binaries/data/mods/public/simulation/components/Trader.js	(revision 0)
+++ binaries/data/mods/public/simulation/components/Trader.js	(revision 0)
@@ -0,0 +1,169 @@
+// This constant used to adjust gain value depending on distance
+const DISTANCE_FACTOR = 1 / 50;
+
+// Additional gain for trading perfromed between markets of different players, in percents
+const INTERNATIONAL_TRADING_ADDITION = 50;
+// Additional gain for ships for each garrisoned trader, in percents
+const GARRISONED_TRADER_ADDITION = 20;
+
+function Trader() {}
+
+Trader.prototype.Schema =
+	"<a:help>Lets the unit generate resouces while moving between markets (or docks in case of water trading).</a:help>" +
+	"<a:example>" +
+		"<MaxDistance>2.0</MaxDistance>" +
+		"<GainMultiplier>1.0</GainMultiplier>" +
+	"</a:example>" +
+	"<element name='MaxDistance' a:help='Max distance from market when performing deal'>" +
+		"<ref name='positiveDecimal'/>" +
+	"</element>" +
+	"<optional>" +
+		"<element name='GainMultiplier' a:help='Additional gain multiplier'>" +
+			"<ref name='positiveDecimal'/>" +
+		"</element>" +
+	"</optional>";
+
+Trader.prototype.Init = function()
+{
+	this.firstMarket = null;
+	this.secondMarket = null;
+	// Gain from one pass between markets
+	this.gain = null;
+	// Selected resource for trading
+	this.preferredGoods = "metal";
+	// Currently carried goods
+	this.goods = { "type": null, "amount": 0 };
+}
+
+Trader.prototype.CalculateGain = function(firstMarket, secondMarket)
+{
+	var cmpFirstMarketPosition = Engine.QueryInterface(firstMarket, IID_Position);
+	var cmpSecondMarketPosition = Engine.QueryInterface(secondMarket, IID_Position);
+	var firstMarketPosition = cmpFirstMarketPosition.GetPosition2D();
+	var secondMarketPosition = cmpSecondMarketPosition.GetPosition2D();
+	// Calculate ordinary Euclidean distance between markets.
+	// We don't use pathfinder, because ordinary distance looks more fair.
+	var distance = Math.sqrt(Math.pow(firstMarketPosition.x - secondMarketPosition.x, 2) + Math.pow(firstMarketPosition.y - secondMarketPosition.y, 2));
+	// We calculate gain as square of distance to encourage trading between remote markets
+	var gain = Math.round(Math.pow(distance * DISTANCE_FACTOR, 2));
+	// If markets belongs to different players, multiple gain to INTERNATIONAL_TRADING_MULTIPLIER
+	var cmpFirstMarketOwnership = Engine.QueryInterface(firstMarket, IID_Ownership);
+	var cmpSecondMarketOwnership = Engine.QueryInterface(secondMarket, IID_Ownership);
+	if (cmpFirstMarketOwnership.GetOwner() != cmpSecondMarketOwnership.GetOwner())
+		gain *= 1 + INTERNATIONAL_TRADING_ADDITION / 100;
+	// For ship increase gain for each garrisoned trader
+	var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+	if (cmpIdentity.HasClass("Ship"))
+	{
+		var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+		if (cmpGarrisonHolder)
+		{
+			var garrisonedTradersCount = 0;
+			for each (var entity in cmpGarrisonHolder.GetEntities())
+			{
+				var cmpGarrisonedUnitTrader = Engine.QueryInterface(entity, IID_Trader);
+				if (cmpGarrisonedUnitTrader)
+					garrisonedTradersCount++;
+			}
+			gain *= 1 + GARRISONED_TRADER_ADDITION * garrisonedTradersCount / 100;
+		}
+	}
+	if (this.template.GainMultiplier)
+		gain *= this.template.GainMultiplier;
+	gain = Math.round(gain);
+	return gain;
+}
+
+// Set target as target market.
+// Return true if at least one of markets was changed.
+Trader.prototype.SetTargetMarket = function(target)
+{
+	var marketsChanged = false;
+	// If we already have both markets - drop them
+	// and use the target as first market
+	if (this.secondMarket)
+	{
+		this.firstMarket = target;
+		this.secondMarket = null;
+		marketsChanged = true;
+	}
+	// If we have only one market and target is different from it,
+	// set the target as second one
+	else if (this.firstMarket)
+	{
+		if (target != this.firstMarket)
+		{
+			this.secondMarket = target;
+			this.gain = this.CalculateGain(this.firstMarket, this.secondMarket);
+			marketsChanged = true;
+		}
+	}
+	// Else we don't have target markets at all,
+	// set the target as first market
+	else
+	{
+		this.firstMarket = target;
+		marketsChanged = true;
+	}
+	if (marketsChanged)
+		this.DropGoods();
+	return marketsChanged;
+}
+
+Trader.prototype.GetFirstMarket = function()
+{
+	return this.firstMarket;
+}
+
+Trader.prototype.GetSecondMarket = function()
+{
+	return this.secondMarket;
+}
+
+Trader.prototype.HasBothMarkets = function()
+{
+	return (this.firstMarket != null) && (this.secondMarket != null);
+}
+
+Trader.prototype.GetPreferredGoods = function()
+{
+	return this.preferredGoods;
+}
+
+Trader.prototype.SetPreferredGoods = function(preferredGoods)
+{
+	this.preferredGoods = preferredGoods;
+}
+
+Trader.prototype.PerformTrade = function()
+{
+	if (this.goods.amount > 0)
+	{
+		var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+		cmpPlayer.AddResource(this.goods.type, this.goods.amount);
+	}
+	this.goods.type = this.preferredGoods;
+	this.goods.amount = this.gain;
+}
+
+Trader.prototype.GetGoods = function()
+{
+	return this.goods;
+}
+
+// Called when the trader stops trading
+Trader.prototype.DropGoods = function()
+{
+	this.goods.amount = 0;
+}
+
+// Get range in which deals with market are available,
+// i.e. trader should be in no more than MaxDistance from market
+// to be able to trade with it.
+Trader.prototype.GetRange = function()
+{
+	return { "min": 0, "max": +this.template.MaxDistance };
+}
+
+Engine.RegisterComponentType(IID_Trader, "Trader", Trader);
+
Index: binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml	(revision 11026)
+++ binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml	(working copy)
@@ -18,8 +18,10 @@
     <GenericName>Trader</GenericName>
     <Rollover>Trade was a very important part of ancient civilisation - effective trading and control of trade routes equaled wealth. Trade took place by many forms from foot to caravans to merchant ships. One of the most notorious examples of the power of trade was the Silk Road.</Rollover>
     <Tooltip>Trades resources between allied Markets.</Tooltip>
-	<Classes datatype="tokens">Trade</Classes>
   </Identity>
+  <Trader>
+    <MaxDistance>2.0</MaxDistance>
+  </Trader>
   <UnitMotion>
     <WalkSpeed>7.0</WalkSpeed>
   </UnitMotion>
Index: binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml	(revision 11026)
+++ binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml	(working copy)
@@ -24,7 +24,7 @@
   <Identity>
     <GenericName>Market</GenericName>
     <Tooltip>Create Trade units and Barter resources.</Tooltip>
-    <Classes datatype="tokens">Town BarterMarket</Classes>
+    <Classes datatype="tokens">Town Market BarterMarket</Classes>
     <Icon>structures/market.png</Icon>
   </Identity>
   <Obstruction>
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	(revision 11026)
+++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_merchant.xml	(working copy)
@@ -38,6 +38,9 @@
     <BarHeight>0.5</BarHeight>
     <HeightOffset>6.0</HeightOffset>
   </StatusBars>
+  <Trader>
+    <MaxDistance>10.0</MaxDistance>
+  </Trader>
   <UnitMotion>
     <WalkSpeed>10.5</WalkSpeed>
   </UnitMotion>
Index: binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml	(revision 11026)
+++ binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml	(working copy)
@@ -27,7 +27,7 @@
   <Identity>
     <GenericName>Dock</GenericName>
     <Tooltip>Build upon a shoreline to construct naval vessels and to open sea trade.</Tooltip>
-    <Classes datatype="tokens">Town</Classes>
+    <Classes datatype="tokens">Town Market SeaMarket</Classes>
     <Icon>structures/dock.png</Icon>
   </Identity>
   <Obstruction>
Index: binaries/data/mods/public/art/textures/cursors/action-setup-trade-route.txt
===================================================================
--- binaries/data/mods/public/art/textures/cursors/action-setup-trade-route.txt	(revision 0)
+++ binaries/data/mods/public/art/textures/cursors/action-setup-trade-route.txt	(revision 0)
@@ -0,0 +1,2 @@
+1 1
+1 1
Index: binaries/data/mods/public/art/textures/cursors/action-setup-trade-route.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: binaries/data/mods/public/art/textures/cursors/action-setup-trade-route.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

