← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/market1 into lp:widelands

 

SirVer has proposed merging lp:~widelands-dev/widelands/market1 into lp:widelands.

Commit message:
First working land-based trading implementation. 

A trade works like this: A player proposes to another players market to initiate a trade. A trade consists of the three pieces of information: what wares will I sent, what wares do I want from you and how often do we exchange. For example I'll send 3 logs whenever you send 1 plank and 5 iron and we do ship these batches 10 times.

Once a trade is initiated, both players will require the correct number of oxen for a batch, i.e. p1 requires 3 (for 3 logs) and p2 requires 6 (1 plank + 5 iron). Once all workers and wares for one batch have arrived in the markets, both markets will launch one caravan and the items gets exchanged.

This is a minimal implementation and not ready for use in-game. Highlights of stuff still missing:
- UI (clicking a market will crash the game).
- denying/accepting a proposed trade. Every proposed trade is auto-accepted atm.
- releasing trade carriers and left-over wares once a trade is cancelled or fulfilled.
- load/save/serialize for networking for all commands and new map objects.

Implemented:
- Adds Lua functions to propose a trade from Lua.
- Adds a PlayerCommand to propose a trade from Lua, but this is still unused.
- Adds logic to trade between two Markets. 
- Adds a test that does a simple trade.

The test is run with the regular regression test runs, but can be run standalone using:

./build/debug/src/widelands --datadir=data --datadir_for_testing=test --scenario=maps/market_trading.wmf --script=maps/market_trading.wmf/scripting/test_simple_trade.lua


Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/market1/+merge/331233
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/market1 into lp:widelands.
=== modified file 'data/tribes/buildings/markets/barbarians/market/init.lua'
--- data/tribes/buildings/markets/barbarians/market/init.lua	2017-09-15 19:53:28 +0000
+++ data/tribes/buildings/markets/barbarians/market/init.lua	2017-09-22 21:01:30 +0000
@@ -50,7 +50,5 @@
       prohibited_till = 1000
    },
 
-   working_positions = {
-      barbarians_ox = 10,
-   },
+   carrier = "barbarians_ox",
 }

=== modified file 'src/economy/request.h'
--- src/economy/request.h	2017-01-25 18:55:59 +0000
+++ src/economy/request.h	2017-09-22 21:01:30 +0000
@@ -123,6 +123,7 @@
 	//  callbacks for WareInstance/Worker code
 	void transfer_finish(Game&, Transfer&);
 	void transfer_fail(Game&, Transfer&);
+	void cancel_transfer(uint32_t idx);
 
 	void set_requirements(const Requirements& r) {
 		requirements_ = r;
@@ -131,13 +132,9 @@
 		return requirements_;
 	}
 
+
 private:
 	int32_t get_base_required_time(EditorGameBase&, uint32_t nr) const;
-
-public:
-	void cancel_transfer(uint32_t idx);
-
-private:
 	void remove_transfer(uint32_t idx);
 	uint32_t find_transfer(Transfer&);
 

=== modified file 'src/economy/ware_instance.cc'
--- src/economy/ware_instance.cc	2017-08-16 10:14:29 +0000
+++ src/economy/ware_instance.cc	2017-09-22 21:01:30 +0000
@@ -309,8 +309,9 @@
 
 	// Update whether we have a Supply or not
 	if (!transfer_ || !transfer_->get_request()) {
-		if (!supply_)
+		if (!supply_) {
 			supply_ = new IdleWareSupply(*this);
+		}
 	} else {
 		delete supply_;
 		supply_ = nullptr;

=== modified file 'src/game_io/game_class_packet.cc'
--- src/game_io/game_class_packet.cc	2017-01-25 18:55:59 +0000
+++ src/game_io/game_class_packet.cc	2017-09-22 21:01:30 +0000
@@ -61,6 +61,8 @@
 	// Write gametime
 	fw.unsigned_32(game.gametime_);
 
+	// TODO(sirver,trading): save/load trade_agreements and related data.
+
 	// We do not care for players, since they were set
 	// on game initialization to match Map::scenario_player_[names|tribes]
 	// or vice versa, so this is handled by map loader

=== modified file 'src/logic/CMakeLists.txt'
--- src/logic/CMakeLists.txt	2017-09-15 12:33:58 +0000
+++ src/logic/CMakeLists.txt	2017-09-22 21:01:30 +0000
@@ -95,14 +95,14 @@
     findimmovable.h
     findnode.cc
     findnode.h
+    game.cc
+    game.h
     game_data_error.cc
     game_data_error.h
-    game.cc
-    game.h
+    map.cc
+    map.h
     map_revision.cc
     map_revision.h
-    map.cc
-    map.h
     mapastar.cc
     mapastar.h
     mapdifferenceregion.cc
@@ -114,18 +114,18 @@
     mapregion.h
     maptriangleregion.cc
     maptriangleregion.h
+    message.h
     message_id.h
     message_queue.h
-    message.h
     nodecaps.h
     objective.h
     path.cc
     path.h
     pathfield.cc
     pathfield.h
-    player_area.h
     player.cc
     player.h
+    player_area.h
     playercommand.cc
     playercommand.h
     playersmanager.cc
@@ -138,6 +138,7 @@
     roadtype.h
     save_handler.cc
     save_handler.h
+    trade_agreement.h
     widelands_geometry_io.cc
     widelands_geometry_io.h
     map_objects/bob.cc

=== modified file 'src/logic/game.cc'
--- src/logic/game.cc	2017-08-30 12:01:47 +0000
+++ src/logic/game.cc	2017-09-22 21:01:30 +0000
@@ -49,6 +49,7 @@
 #include "logic/cmd_luascript.h"
 #include "logic/game_settings.h"
 #include "logic/map_objects/tribes/carrier.h"
+#include "logic/map_objects/tribes/market.h"
 #include "logic/map_objects/tribes/militarysite.h"
 #include "logic/map_objects/tribes/ship.h"
 #include "logic/map_objects/tribes/soldier.h"
@@ -753,6 +754,87 @@
 	   get_gametime(), ship.get_owner()->player_number(), ship.serial()));
 }
 
+void Game::send_player_propose_trade(const Trade& trade) {
+	auto* object = objects().get_object(trade.initiator);
+	assert(object != nullptr);
+	send_player_command(
+	   *new CmdProposeTrade(get_gametime(), object->get_owner()->player_number(), trade));
+}
+
+int Game::propose_trade(const Trade& trade) {
+	// TODO(sirver,trading): Check if a trade is possible (i.e. if there is a
+	// path between the two markets);
+	const int id = next_trade_agreement_id_;
+	++next_trade_agreement_id_;
+
+	auto* initiator = dynamic_cast<Market*>(objects().get_object(trade.initiator));
+	auto* receiver = dynamic_cast<Market*>(objects().get_object(trade.receiver));
+	// This is only ever called through a PlayerCommand and that already made
+	// sure that the objects still exist. Since no time has passed, they should
+	// not have vanished under us.
+	assert(initiator != nullptr);
+	assert(receiver != nullptr);
+
+	receiver->removed.connect(
+	   [this, id](const uint32_t /* serial */) { cancel_trade(id); });
+	initiator->removed.connect(
+	   [this, id](const uint32_t /* serial */) { cancel_trade(id); });
+
+	receiver->send_message(*this, Message::Type::kTradeOfferReceived, receiver->descr().descname(),
+	                       receiver->descr().icon_filename(), receiver->descr().descname(),
+	                       _("This Market received a new trade offer."), true);
+	trade_agreements_[id] = TradeAgreement{TradeAgreement::State::kProposed, trade};
+
+	// TODO(sirver): this should be done through another player_command, but I
+	// want to get to the trade logic implementation now.
+	accept_trade(id);
+	return id;
+}
+
+void Game::accept_trade(const int trade_id) {
+	auto it = trade_agreements_.find(trade_id);
+	if (it == trade_agreements_.end()) {
+		log("Game::accept_trade: Trade %d has vanished. Ignoring.\n", trade_id);
+		return;
+	}
+	const Trade& trade = it->second.trade;
+	auto* initiator = dynamic_cast<Market*>(objects().get_object(trade.initiator));
+	auto* receiver = dynamic_cast<Market*>(objects().get_object(trade.receiver));
+	if (initiator == nullptr || receiver == nullptr) {
+		cancel_trade(trade_id);
+		return;
+	}
+
+	initiator->new_trade(trade_id, trade.send_items, trade.num_batches, trade.receiver);
+	receiver->new_trade(trade_id, trade.received_items, trade.num_batches, trade.initiator);
+
+	// TODO(sirver,trading): Message the users that the trade has been accepted.
+}
+
+void Game::cancel_trade(int trade_id) {
+	// The trade id might be long gone - since we never disconnect from the
+	// 'removed' signal of the two buildings, we might be invoked long after the
+	// trade was deleted for other reasons.
+	const auto it = trade_agreements_.find(trade_id);
+	if (it == trade_agreements_.end()) {
+		return;
+	}
+	const auto& trade = it->second.trade;
+
+	auto* initiator = dynamic_cast<Market*>(objects().get_object(trade.initiator));
+	if (initiator != nullptr) {
+		initiator->cancel_trade(trade_id);
+		// TODO(sirver,trading): Send message to owner that the trade has been canceled.
+	}
+
+	auto* receiver = dynamic_cast<Market*>(objects().get_object(trade.receiver));
+	if (receiver != nullptr) {
+		receiver->cancel_trade(trade_id);
+		// TODO(sirver,trading): Send message to owner that the trade has been canceled.
+	}
+	trade_agreements_.erase(trade_id);
+}
+
 LuaGameInterface& Game::lua() {
 	return static_cast<LuaGameInterface&>(EditorGameBase::lua());
 }

=== modified file 'src/logic/game.h'
--- src/logic/game.h	2017-08-18 00:17:39 +0000
+++ src/logic/game.h	2017-09-22 21:01:30 +0000
@@ -27,6 +27,7 @@
 #include "logic/cmd_queue.h"
 #include "logic/editor_game_base.h"
 #include "logic/save_handler.h"
+#include "logic/trade_agreement.h"
 #include "random/random.h"
 #include "scripting/logic.h"
 
@@ -207,6 +208,7 @@
 	void send_player_ship_explore_island(Ship&, IslandExploreDirection);
 	void send_player_sink_ship(Ship&);
 	void send_player_cancel_expedition_ship(Ship&);
+	void send_player_propose_trade(const Trade& trade);
 
 	InteractivePlayer* get_ipl();
 
@@ -244,6 +246,11 @@
 
 	void set_auto_speed(bool);
 
+	// TODO(sirver,trading): document these functions once the interface settles.
+	int propose_trade(const Trade& trade);
+	void accept_trade(int trade_id);
+	void cancel_trade(int trade_id);
+
 private:
 	void sync_reset();
 
@@ -308,6 +315,9 @@
 	std::unique_ptr<ReplayWriter> replaywriter_;
 
 	GeneralStatsVector general_stats_;
+	int next_trade_agreement_id_ = 1;
+	// Maps from trade agreement id to the agreement.
+	std::map<int, TradeAgreement> trade_agreements_;
 
 	/// For save games and statistics generation
 	std::string win_condition_displayname_;

=== modified file 'src/logic/map_objects/bob.cc'
--- src/logic/map_objects/bob.cc	2017-08-20 08:34:02 +0000
+++ src/logic/map_objects/bob.cc	2017-09-22 21:01:30 +0000
@@ -390,6 +390,10 @@
 	state.ivar1 = 0;
 }
 
+bool Bob::is_idle() {
+	return get_state(taskIdle);
+}
+
 /**
  * Move along a predefined path.
  * \par ivar1 the step number.

=== modified file 'src/logic/map_objects/bob.h'
--- src/logic/map_objects/bob.h	2017-06-24 08:47:46 +0000
+++ src/logic/map_objects/bob.h	2017-09-22 21:01:30 +0000
@@ -278,6 +278,7 @@
 	// TODO(feature-Hasi50): correct (?) Send a signal that may switch to some other \ref Task
 	void send_signal(Game&, char const*);
 	void start_task_idle(Game&, uint32_t anim, int32_t timeout);
+	bool is_idle();
 
 	/// This can fail (and return false). Therefore the caller must check the
 	/// result and find something else for the bob to do. Otherwise there will

=== modified file 'src/logic/map_objects/tribes/market.cc'
--- src/logic/map_objects/tribes/market.cc	2017-09-18 13:43:08 +0000
+++ src/logic/map_objects/tribes/market.cc	2017-09-22 21:01:30 +0000
@@ -19,8 +19,11 @@
 
 #include "logic/map_objects/tribes/market.h"
 
+#include <memory>
+
 #include "base/i18n.h"
 #include "logic/map_objects/tribes/productionsite.h"
+#include "logic/map_objects/tribes/tribes.h"
 
 namespace Widelands {
 
@@ -30,7 +33,12 @@
    : BuildingDescr(init_descname, MapObjectType::MARKET, table, egbase) {
 	i18n::Textdomain td("tribes");
 
-	parse_working_positions(egbase, table.get_table("working_positions").get(), &working_positions_);
+	DescriptionIndex const woi = egbase.tribes().worker_index(table.get_string("carrier"));
+	if (!egbase.tribes().worker_exists(woi)) {
+		throw wexception("invalid");
+	}
+	carrier_ = woi;
+
 	// TODO(sirver,trading): Add actual logic here.
 }
 
@@ -38,10 +46,223 @@
 	return *new Market(*this);
 }
 
+int Market::TradeOrder::num_wares_per_batch() const {
+	int sum = 0;
+	for (const auto& item_pair : items) {
+		sum += item_pair.second;
+	}
+	return sum;
+}
+
+bool Market::TradeOrder::fulfilled() const {
+	return num_shipped_batches == initial_num_batches;
+}
+
+// TODO(sirver,trading): This needs to implement 'set_economy'. Maybe common code can be shared.
 Market::Market(const MarketDescr& the_descr) : Building(the_descr) {
 }
 
 Market::~Market() {
 }
 
+void Market::new_trade(const int trade_id,
+                       const BillOfMaterials& items,
+                       const int num_batches,
+                       const Serial other_side) {
+	trade_orders_[trade_id] = TradeOrder{items, num_batches, 0, other_side, 0, nullptr, {}, {}};
+	auto& trade_order = trade_orders_[trade_id];
+
+	// Request one worker for each item in a batch.
+	trade_order.worker_request.reset(
+	   new Request(*this, descr().carrier(), Market::worker_arrived_callback, wwWORKER));
+	trade_order.worker_request->set_count(trade_order.num_wares_per_batch());
+
+	// Make sure we have a wares queue for each item in this.
+	for (const auto& entry : items) {
+		ensure_wares_queue_exists(entry.first);
+	}
+}
+
+void Market::cancel_trade(const int trade_id) {
+	// TODO(sirver,trading): Launch workers, release no longer required wares and delete now unneeded 'WaresQueue's
+	trade_orders_.erase(trade_id);
+}
+
+void Market::worker_arrived_callback(
+   Game& game, Request& rq, DescriptionIndex /* widx */, Worker* const w, PlayerImmovable& target) {
+	auto& market = dynamic_cast<Market&>(target);
+
+	assert(w);
+	assert(w->get_location(game) == &market);
+
+	for (auto& trade_order_pair : market.trade_orders_) {
+		auto& trade_order = trade_order_pair.second;
+		if (trade_order.worker_request.get() != &rq) {
+			continue;
+		}
+
+		if (rq.get_count() == 0) {
+			// Erase this request.
+			trade_order.worker_request.reset();
+		}
+		w->start_task_idle(game, 0, -1);
+		trade_order.workers.push_back(w);
+		Notifications::publish(NoteBuilding(market.serial(), NoteBuilding::Action::kWorkersChanged));
+		market.try_launching_batch(&game);
+		return;
+	}
+	NEVER_HERE(); // We should have found and handled a match by now.
+}
+
+void Market::ware_arrived_callback(Game& g, InputQueue*, DescriptionIndex, Worker*, void* data) {
+	Market& market = *static_cast<Market*>(data);
+	market.try_launching_batch(&g);
+}
+
+void Market::try_launching_batch(Game* game) {
+	for (auto& pair : trade_orders_) {
+		if (!is_ready_to_launch_batch(pair.first)) {
+			continue;
+		}
+
+		auto* other_market =
+		   dynamic_cast<Market*>(game->objects().get_object(pair.second.other_side));
+		if (other_market == nullptr) {
+			// TODO(sirver,trading): Can this even happen? Where is this function called from.
+			// The other market seems to have vanished. The game tracks this and
+			// should soon delete this trade request from us. We just ignore it.
+			continue;
+		}
+		if (!other_market->is_ready_to_launch_batch(pair.first)) {
+			continue;
+		}
+		launch_batch(pair.first, game);
+		other_market->launch_batch(pair.first, game);
+		break;
+	}
+}
+
+bool Market::is_ready_to_launch_batch(const int trade_id) {
+	const auto it = trade_orders_.find(trade_id);
+	if (it == trade_orders_.end()) {
+		return false;
+	}
+	auto& trade_order = it->second;
+	assert(!trade_order.fulfilled());
+
+	// Do we have all necessary wares for a batch?
+	for (const auto& item_pair : trade_order.items) {
+		const auto wares_it = wares_queue_.find(item_pair.first);
+		if (wares_it == wares_queue_.end()) {
+			return false;
+		}
+		if (wares_it->second->get_filled() < item_pair.second) {
+			return false;
+		}
+	}
+
+	// Do we have enough people to carry wares?
+	int num_available_carriers = 0;
+	for (auto* worker : trade_order.workers) {
+		num_available_carriers += worker->is_idle() ? 1 : 0;
+	}
+	return num_available_carriers == trade_order.num_wares_per_batch();
+}
+
+void Market::launch_batch(const int trade_id, Game* game) {
+	assert(is_ready_to_launch_batch(trade_id));
+	auto& trade_order = trade_orders_.at(trade_id);
+
+	// Do we have all necessary wares for a batch?
+	int worker_index = 0;
+	for (const auto& item_pair : trade_order.items) {
+		for (size_t i = 0; i < item_pair.second; ++i) {
+			Worker* carrier = trade_order.workers.at(worker_index);
+			++worker_index;
+			assert(carrier->is_idle());
+
+			// Give the carrier a ware.
+			WareInstance* ware =
+			   new WareInstance(item_pair.first, game->tribes().get_ware_descr(item_pair.first));
+			ware->init(*game);
+			carrier->set_carried_ware(*game, ware);
+
+			// We have to remove this item from our economy. Otherwise it would be
+			// considered idle (since it has no transport associated with it) and
+			// the engine would want to transfer it to the next warehouse.
+			ware->set_economy(nullptr);
+			wares_queue_.at(item_pair.first)
+			   ->set_filled(wares_queue_.at(item_pair.first)->get_filled() - 1);
+
+			// Send the carrier going.
+			carrier->reset_tasks(*game);
+			carrier->start_task_carry_trade_item(
+			   *game, trade_id, ObjectPointer(game->objects().get_object(trade_order.other_side)));
+		}
+	}
+}
+
+void Market::ensure_wares_queue_exists(int ware_index) {
+	if (wares_queue_.count(ware_index) > 0) {
+		return;
+	}
+	wares_queue_[ware_index] =
+	   std::unique_ptr<WaresQueue>(new WaresQueue(*this, ware_index, kMaxPerItemTradeBatchSize));
+	wares_queue_[ware_index]->set_callback(Market::ware_arrived_callback, this);
+}
+
+InputQueue& Market::inputqueue(DescriptionIndex index, WareWorker ware_worker) {
+	assert(ware_worker == wwWARE);
+	auto it = wares_queue_.find(index);
+	if (it != wares_queue_.end()) {
+		return *it->second;
+	}
+	// The parent will throw an exception.
+	return Building::inputqueue(index, ware_worker);
+}
+
+void Market::cleanup(EditorGameBase& egbase) {
+	for (auto& pair : wares_queue_) {
+		pair.second->cleanup();
+	}
+	Building::cleanup(egbase);
+}
+
+void Market::traded_ware_arrived(const int trade_id,  const DescriptionIndex ware_index, Game* game) {
+	auto& trade_order = trade_orders_.at(trade_id);
+
+	WareInstance* ware = new WareInstance(ware_index, game->tribes().get_ware_descr(ware_index));
+	ware->init(*game);
+
+	// TODO(sirver,trading): This is a hack. We should have a worker that
+	// carriers stuff out. At the moment this assumes this market is barbarians
+	// (which is always correct right now), creates a carrier for each received
+	// ware to drop it off. The carrier then leaves the building and goes home.
+	const WorkerDescr& w_desc =
+	   *game->tribes().get_worker_descr(game->tribes().worker_index("barbarians_carrier"));
+	auto& worker = w_desc.create(*game, owner(), this, position_);
+	worker.start_task_dropoff(*game, *ware);
+	trade_order.received_traded_wares_in_this_batch += 1;
+	owner().ware_produced(ware_index);
+
+	auto* other_market = dynamic_cast<Market*>(game->objects().get_object(trade_order.other_side));
+	assert(other_market != nullptr);
+	other_market->owner().ware_consumed(ware_index, 1);
+	auto& other_trade_order = other_market->trade_orders_.at(trade_id);
+	if (trade_order.received_traded_wares_in_this_batch == other_trade_order.num_wares_per_batch() &&
+			other_trade_order.received_traded_wares_in_this_batch == trade_order.num_wares_per_batch()) {
+		// This batch is completed.
+		++trade_order.num_shipped_batches;
+		trade_order.received_traded_wares_in_this_batch = 0;
+		++other_trade_order.num_shipped_batches;
+		other_trade_order.received_traded_wares_in_this_batch = 0;
+		if (trade_order.fulfilled()) {
+			assert(other_trade_order.fulfilled());
+			// TODO(sirver,trading): This is not quite correct. This should for
+			// example send a differnet message than actually canceling a trade.
+			game->cancel_trade(trade_id);
+		}
+	}
+}
+
 }  // namespace Widelands

=== modified file 'src/logic/map_objects/tribes/market.h'
--- src/logic/map_objects/tribes/market.h	2017-09-15 12:33:58 +0000
+++ src/logic/map_objects/tribes/market.h	2017-09-22 21:01:30 +0000
@@ -20,6 +20,10 @@
 #ifndef WL_LOGIC_MAP_OBJECTS_TRIBES_MARKET_H
 #define WL_LOGIC_MAP_OBJECTS_TRIBES_MARKET_H
 
+#include <memory>
+
+#include "economy/request.h"
+#include "economy/wares_queue.h"
 #include "logic/map_objects/tribes/building.h"
 
 namespace Widelands {
@@ -32,8 +36,10 @@
 
 	Building& create_object() const override;
 
+	DescriptionIndex carrier() const { return carrier_; }
+
 private:
-	BillOfMaterials working_positions_;
+	DescriptionIndex carrier_;
 };
 
 class Market : public Building {
@@ -42,7 +48,55 @@
 	explicit Market(const MarketDescr& descr);
 	~Market() override;
 
+	void new_trade(int trade_id, const BillOfMaterials& items, int num_batches, Serial other_side);
+	void cancel_trade(int trade_id);
+
+	InputQueue& inputqueue(DescriptionIndex, WareWorker) override;
+	void cleanup(EditorGameBase&) override;
+
+	void try_launching_batch(Game* game);
+	void traded_ware_arrived(int trade_id, DescriptionIndex ware_index, Game* game);
+
 private:
+	struct WareRequest {
+		int index;
+		std::unique_ptr<Request> request;
+	};
+
+	struct TradeOrder {
+		BillOfMaterials items;
+		int initial_num_batches;
+		int num_shipped_batches;
+		Serial other_side;
+
+		int received_traded_wares_in_this_batch;
+
+		// The invariant here is that worker.size() + worker_request.get_count()
+		// == 'num_wares_per_batch()'
+		std::unique_ptr<Request> worker_request;
+		std::vector<Worker*> workers;
+
+		std::vector<WareRequest> ware_requests;
+
+		// The number of individual wares in 'items', i.e. the sum of all '.second's.
+		int num_wares_per_batch() const;
+
+		// True if the 'num_shipped_batches' equals the 'initial_num_batches'
+		bool fulfilled() const;
+	};
+
+	static void
+	worker_arrived_callback(Game&, Request&, DescriptionIndex, Worker*, PlayerImmovable&);
+	static void
+	ware_arrived_callback(Game& g, InputQueue* q, DescriptionIndex ware, Worker* worker, void* data);
+
+	void ensure_wares_queue_exists(int ware_index);
+	bool is_ready_to_launch_batch(int trade_id);
+	void launch_batch(int trade_id, Game* game);
+
+	std::map<int, TradeOrder> trade_orders_;  // Key is 'trade_id's.
+	std::map<int, std::unique_ptr<WaresQueue>> wares_queue_; // Key is 'ware_index'.
+
 	DISALLOW_COPY_AND_ASSIGN(Market);
 };
 

=== modified file 'src/logic/map_objects/tribes/productionsite.cc'
--- src/logic/map_objects/tribes/productionsite.cc	2017-09-15 12:33:58 +0000
+++ src/logic/map_objects/tribes/productionsite.cc	2017-09-22 21:01:30 +0000
@@ -49,8 +49,9 @@
 
 constexpr size_t STATISTICS_VECTOR_LENGTH = 20;
 
-}  // namespace
-
+// Parses the descriptions of the working positions from 'items_table' and
+// fills in 'working_positions'. Throws an error if the table contains invalid
+// values.
 void parse_working_positions(const EditorGameBase& egbase,
                              LuaTable* items_table,
                              BillOfMaterials* working_positions) {
@@ -71,6 +72,8 @@
 	}
 }
 
+}  // namespace
+
 /*
 ==============================================================================
 

=== modified file 'src/logic/map_objects/tribes/productionsite.h'
--- src/logic/map_objects/tribes/productionsite.h	2017-09-18 13:43:08 +0000
+++ src/logic/map_objects/tribes/productionsite.h	2017-09-22 21:01:30 +0000
@@ -375,13 +375,6 @@
 	}
 };
 
-// Parses the descriptions of the working positions from 'items_table' and
-// fills in 'working_positions'. Throws an error if the table contains invalid
-// values.
-void parse_working_positions(const EditorGameBase& egbase,
-                             LuaTable* items_table,
-                             BillOfMaterials* working_positions);
-
 }  // namespace Widelands
 
 #endif  // end of include guard: WL_LOGIC_MAP_OBJECTS_TRIBES_PRODUCTIONSITE_H

=== modified file 'src/logic/map_objects/tribes/tribes.cc'
--- src/logic/map_objects/tribes/tribes.cc	2017-09-15 12:33:58 +0000
+++ src/logic/map_objects/tribes/tribes.cc	2017-09-22 21:01:30 +0000
@@ -24,6 +24,7 @@
 #include "base/wexception.h"
 #include "graphic/graphic.h"
 #include "logic/game_data_error.h"
+#include "logic/map_objects/tribes/market.h"
 
 namespace Widelands {
 

=== modified file 'src/logic/map_objects/tribes/tribes.h'
--- src/logic/map_objects/tribes/tribes.h	2017-09-15 12:33:58 +0000
+++ src/logic/map_objects/tribes/tribes.h	2017-09-22 21:01:30 +0000
@@ -38,7 +38,6 @@
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/map_objects/tribes/ware_descr.h"
 #include "logic/map_objects/tribes/warehouse.h"
-#include "logic/map_objects/tribes/market.h"
 #include "logic/map_objects/tribes/worker_descr.h"
 #include "scripting/lua_table.h"
 

=== modified file 'src/logic/map_objects/tribes/worker.cc'
--- src/logic/map_objects/tribes/worker.cc	2017-08-20 17:45:42 +0000
+++ src/logic/map_objects/tribes/worker.cc	2017-09-22 21:01:30 +0000
@@ -47,6 +47,7 @@
 #include "logic/map_objects/terrain_affinity.h"
 #include "logic/map_objects/tribes/carrier.h"
 #include "logic/map_objects/tribes/dismantlesite.h"
+#include "logic/map_objects/tribes/market.h"
 #include "logic/map_objects/tribes/soldier.h"
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/map_objects/tribes/warehouse.h"
@@ -1590,7 +1591,89 @@
  * is finished.
  */
 void Worker::update_task_buildingwork(Game& game) {
-	if (top_state().task == &taskBuildingwork)
+	if (top_state().task == &taskCarryTradeItem)
+		send_signal(game, "update");
+}
+
+// The task when a worker is part of the caravane that is trading items.
+const Bob::Task Worker::taskCarryTradeItem = {
+   "carry_trade_item", static_cast<Bob::Ptr>(&Worker::carry_trade_item_update), nullptr, nullptr, true};
+
+void Worker::start_task_carry_trade_item(Game& game,
+                                         const int trade_id,
+                                         ObjectPointer other_market) {
+	push_task(game, taskCarryTradeItem);
+	auto& state = top_state();
+	state.ivar1 = 0;
+	state.ivar2 = trade_id;
+	state.objvar1 = other_market;
+}
+
+// This is a state machine: leave building, go to the other market, drop off
+// wares, and return.
+void Worker::carry_trade_item_update(Game& game, State& state) {
+	// Reset any signals that are not related to location
+	std::string signal = get_signal();
+	signal_handled();
+	if (!signal.empty()) {
+		// TODO(sirver,trading): Remove once signals are correctly handled.
+		log("carry_trade_item_update: signal received: %s\n", signal.c_str());
+	}
+	if (signal == "evict") {
+		return pop_task(game);
+	}
+
+	// First of all, make sure we're outside
+	if (state.ivar1 == 0) {
+		start_task_leavebuilding(game, false);
+		++state.ivar1;
+		return;
+	}
+
+	auto* other_market = dynamic_cast<Market*>(state.objvar1.get(game));
+	if (state.ivar1 == 1) {
+		// Arrived on site. Move to the building and advance our state.
+		if (other_market->base_flag().get_position() == get_position()) {
+			++state.ivar1;
+			return start_task_move(
+			   game, WALK_NW, descr().get_right_walk_anims(does_carry_ware()), true);
+		}
+
+		// Otherwise continue making progress towards the other market.
+		if (!start_task_movepath(game, other_market->base_flag().get_position(), 5,
+		                         descr().get_right_walk_anims(does_carry_ware()))) {
+			molog("carry_trade_item_update: Could not move to other flag.\n");
+			// TODO(sirver,trading): something needs to happen here.
+		}
+		return;
+	}
+
+	if (state.ivar1 == 2) {
+		WareInstance* const ware = fetch_carried_ware(game);
+		other_market->traded_ware_arrived(state.ivar2, ware->descr_index(), &game);
+		ware->remove(game);
+		++state.ivar1;
+		start_task_move(game, WALK_SE, descr().get_right_walk_anims(does_carry_ware()), true);
+		return;
+	}
+
+	if (state.ivar1 == 3) {
+		++state.ivar1;
+		start_task_return(game, false);
+		return;
+	}
+
+	if (state.ivar1 == 4) {
+		pop_task(game);
+		start_task_idle(game, 0, -1);
+		dynamic_cast<Market*>(get_location(game))->try_launching_batch(&game);
+		return;
+	}
+	NEVER_HERE();
+}
+
+void Worker::update_task_carry_trade_item(Game& game) {
+	if (top_state().task == &taskCarryTradeItem)
 		send_signal(game, "update");
 }
 

=== modified file 'src/logic/map_objects/tribes/worker.h'
--- src/logic/map_objects/tribes/worker.h	2017-06-24 08:47:46 +0000
+++ src/logic/map_objects/tribes/worker.h	2017-09-22 21:01:30 +0000
@@ -169,6 +169,9 @@
 	void start_task_leavebuilding(Game&, bool changelocation);
 	void start_task_fugitive(Game&);
 
+	void start_task_carry_trade_item(Game& game, int trade_id, ObjectPointer other_market);
+	void update_task_carry_trade_item(Game&);
+
 	void
 	start_task_geologist(Game&, uint8_t attempts, uint8_t radius, const std::string& subcommand);
 
@@ -208,6 +211,7 @@
 	static const Task taskFugitive;
 	static const Task taskGeologist;
 	static const Task taskScout;
+	static const Task taskCarryTradeItem;
 
 private:
 	// task details
@@ -232,6 +236,7 @@
 	void fugitive_update(Game&, State&);
 	void geologist_update(Game&, State&);
 	void scout_update(Game&, State&);
+	void carry_trade_item_update(Game&, State&);
 
 	// Program commands
 	bool run_mine(Game&, State&, const Action&);

=== modified file 'src/logic/message.h'
--- src/logic/message.h	2017-02-19 16:56:59 +0000
+++ src/logic/message.h	2017-09-22 21:01:30 +0000
@@ -48,7 +48,8 @@
 		kWarfare,              // everything starting from here is warfare
 		kWarfareSiteDefeated,
 		kWarfareSiteLost,
-		kWarfareUnderAttack
+		kWarfareUnderAttack,
+		kTradeOfferReceived,
 	};
 
 	/**

=== modified file 'src/logic/playercommand.cc'
--- src/logic/playercommand.cc	2017-06-25 21:56:53 +0000
+++ src/logic/playercommand.cc	2017-09-22 21:01:30 +0000
@@ -29,6 +29,7 @@
 #include "io/streamwrite.h"
 #include "logic/game.h"
 #include "logic/map_objects/map_object.h"
+#include "logic/map_objects/tribes/market.h"
 #include "logic/map_objects/tribes/soldier.h"
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/player.h"
@@ -52,6 +53,25 @@
 	return mol.get<T>(object_index).serial();
 }
 
+void serialize_bill_of_materials(const BillOfMaterials& bill, StreamWrite* ser) {
+	ser->unsigned_32(bill.size());
+	for (const WareAmount& amount : bill) {
+		ser->unsigned_8(amount.first);
+		ser->unsigned_32(amount.second);
+	}
+}
+
+BillOfMaterials deserialize_bill_of_materials(StreamRead* des) {
+	BillOfMaterials bill;
+	const int count = des->unsigned_32();
+	for (int i = 0; i < count; ++i) {
+		const auto index = des->unsigned_8();
+		const auto amount = des->unsigned_32();
+		bill.push_back(std::make_pair(index, amount));
+	}
+	return bill;
+}
+
 }  // namespace
 
 // NOTE keep numbers of existing entries as they are to ensure backward compatible savegame loading
@@ -86,7 +106,8 @@
 	PLCMD_SHIP_EXPLORE = 27,
 	PLCMD_SHIP_CONSTRUCT = 28,
 	PLCMD_SHIP_SINK = 29,
-	PLCMD_SHIP_CANCELEXPEDITION = 30
+	PLCMD_SHIP_CANCELEXPEDITION = 30,
+	PLCMD_PROPOSE_TRADE = 31,
 };
 
 /*** class PlayerCommand ***/
@@ -1784,4 +1805,75 @@
 	fw.unsigned_8(ware_);
 	fw.unsigned_8(static_cast<uint8_t>(policy_));
 }
+
+CmdProposeTrade::CmdProposeTrade(uint32_t time, PlayerNumber pn, const Trade& trade)
+   : PlayerCommand(time, pn), trade_(trade) {
+}
+
+CmdProposeTrade::CmdProposeTrade() : PlayerCommand() {
+}
+
+void CmdProposeTrade::execute(Game& game) {
+	Player* plr = game.get_player(sender());
+	if (plr == nullptr) {
+		return;
+	}
+
+	Market* initiator = dynamic_cast<Market*>(game.objects().get_object(trade_.initiator));
+	if (initiator == nullptr) {
+		log("CmdProposeTrade: initiator vanished or is not a market.\n");
+		return;
+	}
+	if (&initiator->owner() != plr) {
+		log("CmdProposeTrade: sender %u, but market owner %u\n", sender(),
+		    initiator->owner().player_number());
+		return;
+	}
+	Market* receiver = dynamic_cast<Market*>(game.objects().get_object(trade_.receiver));
+	if (receiver == nullptr) {
+		log("CmdProposeTrade: receiver vanished or is not a market.\n");
+		return;
+	}
+	if (&initiator->owner() == &receiver->owner()) {
+		log("CmdProposeTrade: Sending and receiving player are the same.\n");
+		return;
+	}
+
+	// TODO(sirver,trading): Maybe check connectivity between markets here and
+	// report errors.
+	game.propose_trade(trade_);
+}
+
+CmdProposeTrade::CmdProposeTrade(StreamRead& des) : PlayerCommand(0, des.unsigned_8()) {
+	trade_.initiator = des.unsigned_32();
+	trade_.receiver = des.unsigned_32();
+	trade_.send_items = deserialize_bill_of_materials(&des);
+	trade_.received_items = deserialize_bill_of_materials(&des);
+	trade_.num_batches = des.signed_32();
+}
+
+void CmdProposeTrade::serialize(StreamWrite& ser) {
+	ser.unsigned_8(PLCMD_PROPOSE_TRADE);
+	ser.unsigned_8(sender());
+	ser.unsigned_32(trade_.initiator);
+	ser.unsigned_32(trade_.receiver);
+	serialize_bill_of_materials(trade_.send_items, &ser);
+	serialize_bill_of_materials(trade_.received_items, &ser);
+	ser.signed_32(trade_.num_batches);
+}
+
+void CmdProposeTrade::read(FileRead& /* fr */,
+                           EditorGameBase& /* egbase */,
+                           MapObjectLoader& /* mol */) {
+	// TODO(sirver,trading): Implement this.
+	NEVER_HERE();
+}
+
+void CmdProposeTrade::write(FileWrite& /* fw */,
+                            EditorGameBase& /* egbase */,
+                            MapObjectSaver& /* mos */) {
+	// TODO(sirver,trading): Implement this.
+	NEVER_HERE();
+}
+
 }

=== modified file 'src/logic/playercommand.h'
--- src/logic/playercommand.h	2017-08-16 13:23:15 +0000
+++ src/logic/playercommand.h	2017-09-22 21:01:30 +0000
@@ -846,6 +846,29 @@
 	DescriptionIndex ware_;
 	Warehouse::StockPolicy policy_;
 };
+
+struct CmdProposeTrade : PlayerCommand {
+	CmdProposeTrade(uint32_t time, PlayerNumber pn, const Trade& trade);
+
+	QueueCommandTypes id() const override {
+		return QueueCommandTypes::kProposeTrade;
+	}
+
+	void execute(Game& game) override;
+
+	// Network (de-)serialization
+	explicit CmdProposeTrade(StreamRead& des);
+	void serialize(StreamWrite& ser) override;
+
+	// Savegame functions
+	CmdProposeTrade();
+	void write(FileWrite&, EditorGameBase&, MapObjectSaver&) override;
+	void read(FileRead&, EditorGameBase&, MapObjectLoader&) override;
+
+private:
+	Trade trade_;
+};
+
 }
 
 #endif  // end of include guard: WL_LOGIC_PLAYERCOMMAND_H

=== modified file 'src/logic/queue_cmd_factory.cc'
--- src/logic/queue_cmd_factory.cc	2017-01-25 18:55:59 +0000
+++ src/logic/queue_cmd_factory.cc	2017-09-22 21:01:30 +0000
@@ -78,6 +78,8 @@
 		return *new CmdEvictWorker();
 	case QueueCommandTypes::kMilitarysiteSetSoldierPreference:
 		return *new CmdMilitarySiteSetSoldierPreference();
+	case QueueCommandTypes::kProposeTrade:
+		return *new CmdProposeTrade();
 	case QueueCommandTypes::kSinkShip:
 		return *new CmdShipSink();
 	case QueueCommandTypes::kShipCancelExpedition:

=== modified file 'src/logic/queue_cmd_ids.h'
--- src/logic/queue_cmd_ids.h	2017-01-25 18:55:59 +0000
+++ src/logic/queue_cmd_ids.h	2017-09-22 21:01:30 +0000
@@ -70,7 +70,8 @@
 
 	kEvictWorker,
 
-	kMilitarysiteSetSoldierPreference,  // 26
+	kMilitarysiteSetSoldierPreference,
+	kProposeTrade, // 27
 
 	kSinkShip = 121,
 	kShipCancelExpedition,

=== added file 'src/logic/trade_agreement.h'
--- src/logic/trade_agreement.h	1970-01-01 00:00:00 +0000
+++ src/logic/trade_agreement.h	2017-09-22 21:01:30 +0000
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2006-2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ */
+
+#ifndef WL_LOGIC_TRADE_AGREEMENT_H
+#define WL_LOGIC_TRADE_AGREEMENT_H
+
+#include "logic/map_objects/map_object.h"
+
+namespace Widelands {
+
+// TODO(sirver,trading): Document everything in here.
+
+// Maximal number of a single ware that can be contained in a trade batch.
+constexpr int kMaxPerItemTradeBatchSize = 15;
+
+struct Trade {
+	BillOfMaterials send_items;
+	BillOfMaterials received_items;
+	int num_batches;
+	Serial initiator;
+	Serial receiver;
+};
+
+// TODO(sirver,trading): This class should probably be private to 'Game'.
+struct TradeAgreement {
+	enum class State {
+		kProposed,
+		kRunning,
+	};
+
+	State state;
+	Trade trade;
+};
+
+}  // namespace Widelands
+
+
+#endif  // end of include guard: WL_LOGIC_TRADE_AGREEMENT_H

=== modified file 'src/map_io/map_buildingdata_packet.cc'
--- src/map_io/map_buildingdata_packet.cc	2017-08-20 08:34:02 +0000
+++ src/map_io/map_buildingdata_packet.cc	2017-09-22 21:01:30 +0000
@@ -339,6 +339,7 @@
 				}
 			}
 
+			// TODO(sirver,trading): Pull out and reuse this for market workers.
 			assert(warehouse.incorporated_workers_.empty());
 			{
 				uint16_t const nrworkers = fr.unsigned_16();

=== modified file 'src/scripting/lua_map.cc'
--- src/scripting/lua_map.cc	2017-09-18 13:43:08 +0000
+++ src/scripting/lua_map.cc	2017-09-22 21:01:30 +0000
@@ -33,6 +33,7 @@
 #include "logic/map_objects/immovable.h"
 #include "logic/map_objects/terrain_affinity.h"
 #include "logic/map_objects/tribes/carrier.h"
+#include "logic/map_objects/tribes/market.h"
 #include "logic/map_objects/tribes/ship.h"
 #include "logic/map_objects/tribes/soldier.h"
 #include "logic/map_objects/tribes/tribes.h"
@@ -594,6 +595,52 @@
 	}
 	return 0;
 }
+
+// Parses a table of name/count pairs as given from Lua.
+void parse_wares_workers(lua_State* L,
+                         int table_index,
+                         const TribeDescr& tribe,
+                         InputMap* ware_workers_list,
+                         bool is_ware) {
+	luaL_checktype(L, table_index, LUA_TTABLE);
+	lua_pushnil(L);
+	while (lua_next(L, table_index) != 0) {
+		if (is_ware) {
+			if (tribe.ware_index(luaL_checkstring(L, -2)) == INVALID_INDEX) {
+				report_error(L, "Illegal ware %s", luaL_checkstring(L, -2));
+			}
+		} else {
+			if (tribe.worker_index(luaL_checkstring(L, -2)) == INVALID_INDEX) {
+				report_error(L, "Illegal worker %s", luaL_checkstring(L, -2));
+			}
+		}
+
+		if (is_ware) {
+			ware_workers_list->insert(
+			   std::make_pair(std::make_pair(tribe.ware_index(luaL_checkstring(L, -2)),
+			                                 Widelands::WareWorker::wwWARE),
+			                  luaL_checkuint32(L, -1)));
+		} else {
+			ware_workers_list->insert(
+			   std::make_pair(std::make_pair(tribe.worker_index(luaL_checkstring(L, -2)),
+			                                 Widelands::WareWorker::wwWORKER),
+			                  luaL_checkuint32(L, -1)));
+		}
+		lua_pop(L, 1);
+	}
+}
+
+BillOfMaterials parse_wares_as_bill_of_material(lua_State* L, int table_index,
+		const TribeDescr& tribe) {
+	InputMap input_map;
+	parse_wares_workers(L, table_index, tribe, &input_map, true /* is_ware */);
+	BillOfMaterials result;
+	for (const auto& pair : input_map) {
+		result.push_back(std::make_pair(pair.first.first, pair.second));
+	}
+	return result;
+}
+
 }  // namespace
 
 /*
@@ -782,7 +829,6 @@
 
 	// We either received, two items string,int:
 	if (nargs == 3) {
-
 		result = RequestedWareWorker::kSingle;
 		if (is_ware) {
 			if (tribe.ware_index(luaL_checkstring(L, 2)) == INVALID_INDEX) {
@@ -803,32 +849,7 @@
 	} else {
 		result = RequestedWareWorker::kList;
 		// or we got a table with name:quantity
-		luaL_checktype(L, 2, LUA_TTABLE);
-		lua_pushnil(L);
-		while (lua_next(L, 2) != 0) {
-			if (is_ware) {
-				if (tribe.ware_index(luaL_checkstring(L, -2)) == INVALID_INDEX) {
-					report_error(L, "Illegal ware %s", luaL_checkstring(L, -2));
-				}
-			} else {
-				if (tribe.worker_index(luaL_checkstring(L, -2)) == INVALID_INDEX) {
-					report_error(L, "Illegal worker %s", luaL_checkstring(L, -2));
-				}
-			}
-
-			if (is_ware) {
-				ware_workers_list->insert(
-				   std::make_pair(std::make_pair(tribe.ware_index(luaL_checkstring(L, -2)),
-				                                 Widelands::WareWorker::wwWARE),
-				                  luaL_checkuint32(L, -1)));
-			} else {
-				ware_workers_list->insert(
-				   std::make_pair(std::make_pair(tribe.worker_index(luaL_checkstring(L, -2)),
-				                                 Widelands::WareWorker::wwWORKER),
-				                  luaL_checkuint32(L, -1)));
-			}
-			lua_pop(L, 1);
-		}
+		parse_wares_workers(L, 2, tribe, ware_workers_list, is_ware);
 	}
 	return result;
 }
@@ -2762,6 +2783,8 @@
    trading over land with other players. See the parent classes for more
    properties.
 */
+// TODO(sirver,trading): Expose the properties of MarketDescription here once
+// the interface settles.
 const char LuaMarketDescription::className[] = "MarketDescription";
 const MethodType<LuaMarketDescription> LuaMarketDescription::Methods[] = {
    {nullptr, nullptr},
@@ -5083,6 +5106,7 @@
 */
 const char LuaMarket::className[] = "Market";
 const MethodType<LuaMarket> LuaMarket::Methods[] = {
+	METHOD(LuaMarket, propose_trade),
    // TODO(sirver,trading): Implement and fix documentation.
    // METHOD(LuaMarket, set_wares),
    // METHOD(LuaMarket, get_wares),
@@ -5106,6 +5130,35 @@
  ==========================================================
  */
 
+/* RST
+   .. method:: propose_trade(other_market, num_batches, send_items, received_items)
+
+      TODO(sirver,trading): document
+
+      :returns: :const:`nil`
+*/
+int LuaMarket::propose_trade(lua_State* L) {
+	if (lua_gettop(L) != 5) {
+		report_error(L, "Takes 4 arguments.");
+	}
+	Game& game = get_game(L);
+	Market* self = get(L, game);
+	Market* other_market = (*get_user_class<LuaMarket>(L, 2))->get(L, game);
+	const int num_batches = luaL_checkinteger(L, 3);
+
+	const BillOfMaterials send_items = parse_wares_as_bill_of_material(L, 4, self->owner().tribe());
+	// TODO(sirver,trading): unsure if correct. Test inter-tribe trading, i.e.
+	// barbarians trading with empire, but shipping atlantean only wares.
+	const BillOfMaterials received_items = parse_wares_as_bill_of_material(L, 5, self->owner().tribe());
+	const int trade_id = game.propose_trade(
+	   Trade{send_items, received_items, num_batches, self->serial(), other_market->serial()});
+
+	// TODO(sirver,trading): Wrap 'Trade' into its own Lua class?
+	lua_pushint32(L, trade_id);
+	return 1;
+}
+
+
 /*
  ==========================================================
  C METHODS

=== modified file 'src/scripting/lua_map.h'
--- src/scripting/lua_map.h	2017-09-20 21:27:25 +0000
+++ src/scripting/lua_map.h	2017-09-22 21:01:30 +0000
@@ -29,6 +29,7 @@
 #include "logic/game.h"
 #include "logic/map_objects/tribes/constructionsite.h"
 #include "logic/map_objects/tribes/dismantlesite.h"
+#include "logic/map_objects/tribes/market.h"
 #include "logic/map_objects/tribes/militarysite.h"
 #include "logic/map_objects/tribes/productionsite.h"
 #include "logic/map_objects/tribes/ship.h"
@@ -1125,6 +1126,7 @@
 	/*
 	 * Lua Methods
 	 */
+	int propose_trade(lua_State* L);
 
 	/*
 	 * C Methods

=== modified file 'src/wui/game_message_menu.cc'
--- src/wui/game_message_menu.cc	2017-08-17 15:34:45 +0000
+++ src/wui/game_message_menu.cc	2017-09-22 21:01:30 +0000
@@ -487,6 +487,7 @@
 	case Widelands::Message::Type::kWarfareSiteDefeated:
 	case Widelands::Message::Type::kWarfareSiteLost:
 	case Widelands::Message::Type::kWarfareUnderAttack:
+	case Widelands::Message::Type::kTradeOfferReceived:
 		set_filter_messages_tooltips();
 		message_filter_ = Widelands::Message::Type::kAllMessages;
 		geologistsbtn_->set_perm_pressed(false);
@@ -580,6 +581,7 @@
 	case Widelands::Message::Type::kWarfareSiteDefeated:
 	case Widelands::Message::Type::kWarfareSiteLost:
 	case Widelands::Message::Type::kWarfareUnderAttack:
+	case Widelands::Message::Type::kTradeOfferReceived:
 		return "images/wui/messages/message_new.png";
 	}
 	NEVER_HERE();

=== modified file 'src/wui/ware_statistics_menu.cc'
--- src/wui/ware_statistics_menu.cc	2017-08-08 17:39:40 +0000
+++ src/wui/ware_statistics_menu.cc	2017-09-22 21:01:30 +0000
@@ -103,8 +103,7 @@
                       &registry,
                       kPlotWidth + 2 * kSpacing,
                       270,
-                      _("Ware Statistics")),
-     parent_(&parent) {
+                      _("Ware Statistics")) {
 	uint8_t const nr_wares = parent.get_player()->egbase().tribes().nrwares();
 
 	// Init color sets

=== modified file 'src/wui/ware_statistics_menu.h'
--- src/wui/ware_statistics_menu.h	2017-08-18 14:27:26 +0000
+++ src/wui/ware_statistics_menu.h	2017-09-22 21:01:30 +0000
@@ -37,7 +37,6 @@
 	void set_time(int32_t);
 
 private:
-	InteractivePlayer* parent_;
 	WuiPlotArea* plot_production_;
 	WuiPlotArea* plot_consumption_;
 	WuiPlotArea* plot_stock_;

=== modified file 'test/maps/market_trading.wmf/scripting/init.lua'
--- test/maps/market_trading.wmf/scripting/init.lua	2017-09-18 13:43:08 +0000
+++ test/maps/market_trading.wmf/scripting/init.lua	2017-09-22 21:01:30 +0000
@@ -1,6 +1,6 @@
-include "scripting/lunit.lua"
 include "scripting/coroutine.lua"
 include "scripting/infrastructure.lua"
+include "scripting/lunit.lua"
 include "scripting/ui.lua"
 
 game = wl.Game()
@@ -22,7 +22,17 @@
    end
 end
 
+function place_markets()
+   prefilled_buildings(p1, { "barbarians_market", 24, 25 })
+   market_p1 = map:get_field(24, 25).immovable
+   connected_road(p1, market_p1.flag, "l,l|", true)
+
+   prefilled_buildings(p2, { "barbarians_market", 29, 25 })
+   market_p2 = map:get_field(29, 25).immovable
+   connected_road(p2, market_p2.flag, "r,r|", true)
+end
+
 full_headquarters(p1, 22, 25)
-full_headquarters(p2, 32, 25)
+full_headquarters(p2, 31, 25)
 
 game.desired_speed = 50000

=== renamed file 'test/maps/market_trading.wmf/scripting/test_market_can_be_build.lua' => 'test/maps/market_trading.wmf/scripting/test_market_can_be_built.lua'
=== added file 'test/maps/market_trading.wmf/scripting/test_simple_trade.lua'
--- test/maps/market_trading.wmf/scripting/test_simple_trade.lua	1970-01-01 00:00:00 +0000
+++ test/maps/market_trading.wmf/scripting/test_simple_trade.lua	2017-09-22 21:01:30 +0000
@@ -0,0 +1,39 @@
+run(function()
+   sleep(2000)
+   place_markets()
+   market_p2:propose_trade(market_p1, 5, { log = 3 }, { granite = 2, iron = 1 })
+
+   local p1_initial = {
+      iron = p1:get_wares("iron"),
+      log = p1:get_wares("log"),
+      granite = p1:get_wares("granite"),
+   }
+   local p2_initial = {
+      iron = p1:get_wares("iron"),
+      log = p1:get_wares("log"),
+      granite = p1:get_wares("granite"),
+   }
+
+   -- We await until one ware we trade has the right count for one player.
+   -- Then, we'll sleep half as long as we already waited to make sure that no
+   -- additional batches are shipped. Then we check all stocks for the correct
+   -- numbers.
+   local start_time = game.time
+   while p2:get_wares("iron") - p2_initial["iron"] < 5 do 
+      sleep(10000)
+   end
+
+   sleep(math.ceil((game.time - start_time) / 2))
+
+   assert_equal(5, p2:get_wares("iron") - p2_initial["iron"])
+   assert_equal(10, p2:get_wares("granite") - p2_initial["granite"])
+   assert_equal(-15, p2:get_wares("log") - p2_initial["log"])
+
+   assert_equal(-5, p1:get_wares("iron") - p1_initial["iron"])
+   assert_equal(-10, p1:get_wares("granite") - p1_initial["granite"])
+   assert_equal(15, p1:get_wares("log") - p1_initial["log"])
+
+   print("# All Tests passed.")
+   wl.ui.MapView():close()
+end)
+


Follow ups