← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/bug-1358880-ship-statistics into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/bug-1358880-ship-statistics into lp:widelands with lp:~widelands-dev/widelands/ships_optr as a prerequisite.

Commit message:
Adds a new Ship Statistics window.

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1358880 in widelands: "List of ships"
  https://bugs.launchpad.net/widelands/+bug/1358880
  Bug #1618617 in widelands: "Idle ships shows "Shipping" in statistic mode = "s"-key"
  https://bugs.launchpad.net/widelands/+bug/1618617
  Bug #1749567 in widelands: "heap-use-after-free when sinking a ship"
  https://bugs.launchpad.net/widelands/+bug/1749567

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/bug-1358880-ship-statistics/+merge/343293

Finally, ths ship list.

lp:~widelands-dev/widelands/ships_optr needs to go in first.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/bug-1358880-ship-statistics into lp:widelands.
=== added file 'data/images/wui/editor/fsel_editor_set_port_space.png'
Binary files data/images/wui/editor/fsel_editor_set_port_space.png	1970-01-01 00:00:00 +0000 and data/images/wui/editor/fsel_editor_set_port_space.png	2018-04-16 08:14:47 +0000 differ
=== removed file 'data/images/wui/editor/fsel_editor_set_port_space.png'
Binary files data/images/wui/editor/fsel_editor_set_port_space.png	2018-01-05 12:07:27 +0000 and data/images/wui/editor/fsel_editor_set_port_space.png	1970-01-01 00:00:00 +0000 differ
=== added file 'data/images/wui/editor/fsel_editor_unset_port_space.png'
Binary files data/images/wui/editor/fsel_editor_unset_port_space.png	1970-01-01 00:00:00 +0000 and data/images/wui/editor/fsel_editor_unset_port_space.png	2018-04-16 08:14:47 +0000 differ
=== removed file 'data/images/wui/editor/fsel_editor_unset_port_space.png'
Binary files data/images/wui/editor/fsel_editor_unset_port_space.png	2018-01-05 12:07:27 +0000 and data/images/wui/editor/fsel_editor_unset_port_space.png	1970-01-01 00:00:00 +0000 differ
=== added file 'data/images/wui/ship/ship_construct_port_space.png'
Binary files data/images/wui/ship/ship_construct_port_space.png	1970-01-01 00:00:00 +0000 and data/images/wui/ship/ship_construct_port_space.png	2018-04-16 08:14:47 +0000 differ
=== added file 'data/images/wui/stats/ship_stats_idle.png'
Binary files data/images/wui/stats/ship_stats_idle.png	1970-01-01 00:00:00 +0000 and data/images/wui/stats/ship_stats_idle.png	2018-04-16 08:14:47 +0000 differ
=== added file 'data/images/wui/stats/ship_stats_shipping.png'
Binary files data/images/wui/stats/ship_stats_shipping.png	1970-01-01 00:00:00 +0000 and data/images/wui/stats/ship_stats_shipping.png	2018-04-16 08:14:47 +0000 differ
=== modified file 'data/tribes/scripting/help/controls.lua'
--- data/tribes/scripting/help/controls.lua	2017-12-06 08:16:46 +0000
+++ data/tribes/scripting/help/controls.lua	2018-04-16 08:14:47 +0000
@@ -21,6 +21,16 @@
                -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
                dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Left-click on Button")), _"Skip confirmation dialog")) ..
 
+         h2(_"Table Control") ..
+         h3(_"In tables that allow the selection of multiple entries, the following key combinations are available:") ..
+         p(
+               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
+               dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Click")), pgettext("table_control", "Select multiple entries")) ..
+               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
+               dl(help_format_hotkey(pgettext("hotkey", "Shift + Click")), pgettext("table_control", "Select a range of entries")) ..
+               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
+               dl(help_format_hotkey(pgettext("hotkey", "Ctrl + A")), pgettext("table_control", "Select all entries"))) ..
+
          h2(_"Road Control") ..
          p(
                -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
@@ -52,6 +62,8 @@
                dl(help_format_hotkey("I"), _"Toggle stock inventory") ..
                -- TRANSLATORS: This is an access key combination. The hotkey is 'b'
                dl(help_format_hotkey("B"), _"Toggle building statistics") ..
+               -- TRANSLATORS: This is an access key combination. The hotkey is 'p'
+               dl(help_format_hotkey("E"), _"Toggle seafaring statistics") ..
                -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
                dl(help_format_hotkey(pgettext("hotkey", "Home")), _"Center main mapview on starting location") ..
                -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
@@ -72,18 +84,8 @@
                dl(help_format_hotkey(pgettext("hotkey", "F6")), _"Show the debug console (only in debug-builds)")
          ) ..
 
-         h2(_"Table Control") ..
-         h3(_"In tables that allow the selection of multiple entries, the following key combinations are available:") ..
-         p(
-               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
-               dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Click")), pgettext("table_control", "Select multiple entries")) ..
-               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
-               dl(help_format_hotkey(pgettext("hotkey", "Shift + Click")), pgettext("table_control", "Select a range of entries")) ..
-               -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
-               dl(help_format_hotkey(pgettext("hotkey", "Ctrl + A")), pgettext("table_control", "Select all entries"))) ..
-
+         -- TRANSLATORS: Heading in "Controls" help
          h2(_"Message Window") ..
-         h3(_"In the message window, the following additional shortcuts are available:") ..
          p(
                -- TRANSLATORS: This is the helptext for an access key combination.
                dl(help_format_hotkey(pgettext("hotkey", "Alt + 0")), _"Show all messages") ..
@@ -101,5 +103,28 @@
                dl(help_format_hotkey("G"), _"Jump to the location corresponding to the current message") ..
                -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
                dl(help_format_hotkey(pgettext("hotkey", "Delete")), _"Archive/Restore the current message")
-          )
+          ) ..
+
+         -- TRANSLATORS: Heading in "Controls" help
+         h2(_"Ship Statistics") ..
+         p(
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey(pgettext("hotkey", "Alt + 0")), _"Show all ships") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey(pgettext("hotkey", "Alt + 1")), _"Show idle ships") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey(pgettext("hotkey", "Alt + 2")), _"Show ships shipping wares and workers") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey(pgettext("hotkey", "Alt + 3")), _"Show waiting expeditions") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey(pgettext("hotkey", "Alt + 4")), _"Show scouting expeditions") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey(pgettext("hotkey", "Alt + 5")), _"Show colonizing expeditions and expeditions with port space found") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey("G"), _"Center the map on the selected ship") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey("O"), _"Go to the selected ship and open its window") ..
+               -- TRANSLATORS: This is the helptext for an access key combination.
+               dl(help_format_hotkey("W"), _"Watch the selected ship")
+         )
 }

=== modified file 'src/ai/defaultai.cc'
--- src/ai/defaultai.cc	2018-04-08 22:33:43 +0000
+++ src/ai/defaultai.cc	2018-04-16 08:14:47 +0000
@@ -167,8 +167,8 @@
 		});
 
 	// Subscribe to ShipNotes.
-	shipnotes_subscriber_ = Notifications::subscribe<NoteShipMessage>([this](
-	   const NoteShipMessage& note) {
+	shipnotes_subscriber_ = Notifications::subscribe<NoteShip>([this](
+	   const NoteShip& note) {
 
 		// in a short time between start and late_initialization the player
 		// can get notes that can not be processed.
@@ -180,13 +180,13 @@
 			return;
 		}
 
-		switch (note.message) {
+		switch (note.action) {
 
-		case NoteShipMessage::Message::kGained:
+		case NoteShip::Action::kGained:
 			gain_ship(*note.ship, NewShip::kBuilt);
 			break;
 
-		case NoteShipMessage::Message::kLost:
+		case NoteShip::Action::kLost:
 			for (std::deque<ShipObserver>::iterator i = allships.begin(); i != allships.end(); ++i) {
 				if (i->ship == note.ship) {
 					allships.erase(i);
@@ -195,15 +195,19 @@
 			}
 			break;
 
-		case NoteShipMessage::Message::kWaitingForCommand:
+		case NoteShip::Action::kWaitingForCommand:
 			for (std::deque<ShipObserver>::iterator i = allships.begin(); i != allships.end(); ++i) {
 				if (i->ship == note.ship) {
 					i->waiting_for_command_ = true;
 					break;
 				}
 			}
+		default:
+			// Do nothing
+			break;
 		}
 	});
+
 }
 
 DefaultAI::~DefaultAI() {

=== modified file 'src/ai/defaultai.h'
--- src/ai/defaultai.h	2018-04-08 22:33:43 +0000
+++ src/ai/defaultai.h	2018-04-16 08:14:47 +0000
@@ -414,7 +414,7 @@
 	   outofresource_subscriber_;
 	std::unique_ptr<Notifications::Subscriber<Widelands::NoteTrainingSiteSoldierTrained>>
 	   soldiertrained_subscriber_;
-	std::unique_ptr<Notifications::Subscriber<Widelands::NoteShipMessage>> shipnotes_subscriber_;
+	std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> shipnotes_subscriber_;
 };
 
 #endif  // end of include guard: WL_AI_DEFAULTAI_H

=== modified file 'src/logic/map_objects/tribes/ship.cc'
--- src/logic/map_objects/tribes/ship.cc	2018-04-07 16:59:00 +0000
+++ src/logic/map_objects/tribes/ship.cc	2018-04-16 08:14:47 +0000
@@ -133,7 +133,6 @@
 }
 
 Ship::~Ship() {
-	Notifications::publish(NoteShipWindow(serial(), NoteShipWindow::Action::kClose));
 }
 
 PortDock* Ship::get_destination(EditorGameBase& egbase) const {
@@ -155,12 +154,13 @@
 bool Ship::init(EditorGameBase& egbase) {
 	Bob::init(egbase);
 	init_fleet(egbase);
-	Notifications::publish(NoteShipMessage(this, NoteShipMessage::Message::kGained));
 	assert(get_owner());
+	get_owner()->add_ship(serial());
 
 	// Assigning a ship name
 	shipname_ = get_owner()->pick_shipname();
 	molog("New ship: %s\n", shipname_.c_str());
+	Notifications::publish(NoteShip(this, NoteShip::Action::kGained));
 	return true;
 }
 
@@ -182,12 +182,17 @@
 		fleet_->remove_ship(egbase, this);
 	}
 
+	Player* owner = get_owner();
+	if (owner != nullptr) {
+		owner->remove_ship(serial());
+	}
+
 	while (!items_.empty()) {
 		items_.back().remove(egbase);
 		items_.pop_back();
 	}
 
-	Notifications::publish(NoteShipMessage(this, NoteShipMessage::Message::kLost));
+	Notifications::publish(NoteShip(this, NoteShip::Action::kLost));
 
 	Bob::cleanup(egbase);
 }
@@ -279,7 +284,7 @@
 	case ShipStates::kSinkAnimation:
 		// The sink animation has been played, so finally remove the ship from the map
 		pop_task(game);
-		remove(game);
+		schedule_destroy(game);
 		return;
 	}
 	// if the real update function failed (e.g. nothing to transport), the ship goes idle
@@ -304,6 +309,7 @@
 		destination_ = nullptr;
 		dst->ship_arrived(game, *this);
 		start_task_idle(game, descr().main_animation(), 250);
+		Notifications::publish(NoteShip(this, NoteShip::Action::kDestinationChanged));
 		return true;
 	}
 
@@ -411,17 +417,13 @@
 			}
 		} while (mr.advance(*map));
 
+		expedition_->seen_port_buildspaces = temp_port_buildspaces;
 		if (new_port_space) {
-			ship_state_ = ShipStates::kExpeditionPortspaceFound;
+			set_ship_state_and_notify(ShipStates::kExpeditionPortspaceFound, NoteShip::Action::kWaitingForCommand);
 			send_message(game, _("Port Space"), _("Port Space Found"),
 			             _("An expedition ship found a new port build space."),
 			             "images/wui/editor/fsel_editor_set_port_space.png");
 		}
-		expedition_->seen_port_buildspaces = temp_port_buildspaces;
-		if (new_port_space) {
-			Notifications::publish(
-			   NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
-		}
 	}
 }
 
@@ -532,17 +534,13 @@
 				} else {
 					// Check whether the island was completely surrounded
 					if (get_position() == expedition_->exploration_start) {
+						set_ship_state_and_notify(ShipStates::kExpeditionWaiting, NoteShip::Action::kWaitingForCommand);
 						send_message(game,
 						             /** TRANSLATORS: A ship has circumnavigated an island and is waiting
 						                for orders */
 						             pgettext("ship", "Waiting"), _("Island Circumnavigated"),
 						             _("An expedition ship sailed around its island without any events."),
 						             "images/wui/ship/ship_explore_island_cw.png");
-						ship_state_ = ShipStates::kExpeditionWaiting;
-
-						Notifications::publish(
-						   NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
-
 						return start_task_idle(game, descr().main_animation(), 1500);
 					}
 				}
@@ -582,10 +580,10 @@
 						}
 				}
 				// if we are here, it seems something really strange happend.
-				log("WARNING: ship %s was not able to start exploration. Entering WAIT mode.\n",
-				    shipname_.c_str());
-				ship_state_ = ShipStates::kExpeditionWaiting;
-				return start_task_idle(game, descr().main_animation(), 1500);
+				log("WARNING: ship %s was not able to start exploration. Entering WAIT mode.", shipname_.c_str());
+				set_ship_state_and_notify(ShipStates::kExpeditionWaiting, NoteShip::Action::kWaitingForCommand);
+				start_task_idle(game, descr().main_animation(), 1500);
+				return;
 			}
 		} else {  // scouting towards a specific direction
 			if (exp_dir_swimmable(expedition_->scouting_direction)) {
@@ -595,7 +593,7 @@
 				return;
 			}
 			// coast reached
-			ship_state_ = ShipStates::kExpeditionWaiting;
+			set_ship_state_and_notify(ShipStates::kExpeditionWaiting, NoteShip::Action::kWaitingForCommand);
 			start_task_idle(game, descr().main_animation(), 1500);
 			// Send a message to the player, that a new coast was reached
 			send_message(game,
@@ -603,10 +601,6 @@
 			             _("Land Ahoy!"), _("Coast Reached"),
 			             _("An expedition ship reached a coast and is waiting for further commands."),
 			             "images/wui/ship/ship_scout_ne.png");
-
-			Notifications::publish(
-			   NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
-
 			return;
 		}
 	}
@@ -710,6 +704,13 @@
 	NEVER_HERE();
 }
 
+void Ship::set_ship_state_and_notify(ShipStates state, NoteShip::Action action) {
+	if (ship_state_ != state) {
+		ship_state_ = state;
+		Notifications::publish(NoteShip(this, action));
+	}
+}
+
 void Ship::set_economy(Game& game, Economy* e) {
 	// Do not check here that the economy actually changed, because on loading
 	// we rely that wares really get reassigned our economy.
@@ -730,6 +731,7 @@
 	      items_.size());
 	destination_ = &pd;
 	send_signal(game, "wakeup");
+	Notifications::publish(NoteShip(this, NoteShip::Action::kDestinationChanged));
 }
 
 void Ship::add_item(Game& game, const ShippingItem& item) {
@@ -847,14 +849,14 @@
 	             pgettext("ship", "Expedition"), _("Expedition Ready"),
 	             _("An expedition ship is waiting for your commands."),
 	             "images/wui/buildings/start_expedition.png");
-	Notifications::publish(NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
+	Notifications::publish(NoteShip(this, NoteShip::Action::kWaitingForCommand));
 }
 
 /// Initializes / changes the direction of scouting to @arg direction
 /// @note only called via player command
 void Ship::exp_scouting_direction(Game&, WalkingDir scouting_direction) {
 	assert(expedition_);
-	ship_state_ = ShipStates::kExpeditionScouting;
+	set_ship_state_and_notify(ShipStates::kExpeditionScouting, NoteShip::Action::kDestinationChanged);
 	expedition_->scouting_direction = scouting_direction;
 	expedition_->island_exploration = false;
 }
@@ -880,7 +882,7 @@
 	for (auto& tree : trees) {
 		tree.object->remove(game);
 	}
-	ship_state_ = ShipStates::kExpeditionColonizing;
+	set_ship_state_and_notify(ShipStates::kExpeditionColonizing, NoteShip::Action::kDestinationChanged);
 }
 
 /// Initializes / changes the direction the island exploration in @arg island_explore_direction
@@ -888,7 +890,7 @@
 /// @note only called via player command
 void Ship::exp_explore_island(Game&, IslandExploreDirection island_explore_direction) {
 	assert(expedition_);
-	ship_state_ = ShipStates::kExpeditionScouting;
+	set_ship_state_and_notify(ShipStates::kExpeditionScouting, NoteShip::Action::kDestinationChanged);
 	expedition_->island_explore_direction = island_explore_direction;
 	expedition_->scouting_direction = WalkingDir::IDLE;
 	expedition_->island_exploration = true;
@@ -945,7 +947,7 @@
 			}
 		}
 
-		Notifications::publish(NoteShipWindow(serial(), NoteShipWindow::Action::kNoPortLeft));
+		Notifications::publish(NoteShip(this, NoteShip::Action::kNoPortLeft));
 		return;
 	}
 	assert(get_economy() && get_economy() != expedition_->economy.get());
@@ -962,7 +964,6 @@
 	// Running colonization has the highest priority + a sink request is only valid once
 	if (!state_is_sinkable())
 		return;
-	Notifications::publish(NoteShipWindow(serial(), NoteShipWindow::Action::kClose));
 	ship_state_ = ShipStates::kSinkRequest;
 	// Make sure the ship is active and close possible open windows
 	ship_wakeup(game);
@@ -980,8 +981,13 @@
 	if (draw_text & TextToDraw::kStatistics) {
 		switch (ship_state_) {
 		case (ShipStates::kTransport):
-			/** TRANSLATORS: This is a ship state */
-			statistics_string = pgettext("ship_state", "Shipping");
+			if (destination_.is_set()) {
+				/** TRANSLATORS: This is a ship state. The ship is currently transporting wares. */
+				statistics_string = pgettext("ship_state", "Shipping");
+			} else {
+				/** TRANSLATORS: This is a ship state. The ship is ready to transport wares, but has nothing to do. */
+				statistics_string = pgettext("ship_state", "Idle");
+			}
 			break;
 		case (ShipStates::kExpeditionWaiting):
 			/** TRANSLATORS: This is a ship state. An expedition is waiting for your commands. */
@@ -1182,6 +1188,7 @@
 	// economy. Also, we might are on an expedition which means that we just now
 	// created the economy of this ship and must inform all wares.
 	ship.set_economy(dynamic_cast<Game&>(egbase()), ship.economy_);
+	ship.get_owner()->add_ship(ship.serial());
 }
 
 MapObject::Loader* Ship::load(EditorGameBase& egbase, MapObjectLoader& mol, FileRead& fr) {

=== modified file 'src/logic/map_objects/tribes/ship.h'
--- src/logic/map_objects/tribes/ship.h	2018-04-07 16:59:00 +0000
+++ src/logic/map_objects/tribes/ship.h	2018-04-16 08:14:47 +0000
@@ -41,29 +41,16 @@
 	kNotSet
 };
 
-struct NoteShipMessage {
-	CAN_BE_SENT_AS_NOTE(NoteId::ShipMessage)
+struct NoteShip {
+	CAN_BE_SENT_AS_NOTE(NoteId::Ship)
 
 	Ship* ship;
 
-	enum class Message { kLost, kGained, kWaitingForCommand };
-	Message message;
-
-	NoteShipMessage(Ship* const init_ship, const Message& init_message)
-	   : ship(init_ship), message(init_message) {
-	}
-};
-
-struct NoteShipWindow {
-	CAN_BE_SENT_AS_NOTE(NoteId::ShipWindow)
-
-	Serial serial;
-
-	enum class Action { kClose, kNoPortLeft };
-	const Action action;
-
-	NoteShipWindow(Serial init_serial, const Action& init_action)
-	   : serial(init_serial), action(init_action) {
+	enum class Action { kDestinationChanged, kWaitingForCommand, kNoPortLeft, kLost, kGained };
+	Action action;
+
+	NoteShip(Ship* const init_ship, const Action& init_action)
+		: ship(init_ship), action(init_action) {
 	}
 };
 
@@ -263,6 +250,8 @@
 	bool ship_update_transport(Game&, State&);
 	void ship_update_expedition(Game&, State&);
 	void ship_update_idle(Game&, State&);
+	/// Set the ship's state to 'state' and if the ship state has changed, publish a notification.
+	void set_ship_state_and_notify(ShipStates state, NoteShip::Action action);
 
 	bool init_fleet(EditorGameBase&);
 	void set_fleet(Fleet* fleet);

=== modified file 'src/logic/player.cc'
--- src/logic/player.cc	2018-04-07 16:59:00 +0000
+++ src/logic/player.cc	2018-04-16 08:14:47 +0000
@@ -23,6 +23,7 @@
 #include <memory>
 
 #include <boost/bind.hpp>
+#include <boost/format.hpp>
 #include <boost/signals2.hpp>
 
 #include "base/i18n.h"
@@ -372,6 +373,18 @@
 	game->cmdqueue().enqueue(new CmdDeleteMessage(game->get_gametime(), player_number_, message_id));
 }
 
+const std::set<Serial>& Player::ships() const {
+	return ships_;
+}
+void Player::add_ship(Serial ship) {
+	ships_.insert(ship);
+}
+void Player::remove_ship(Serial ship) {
+	if (ships_.count(ship) == 1) {
+		ships_.erase(ship);
+	}
+}
+
 /*
 ===============
 Return filtered buildcaps that take the player's territory into account.
@@ -1303,7 +1316,7 @@
 		remaining_shipnames_.erase(it);
 		return new_name;
 	}
-	return "Ship";
+	return (boost::format(pgettext("shipname", "Ship %d")) % ships_.size()).str();
 }
 
 /**

=== modified file 'src/logic/player.h'
--- src/logic/player.h	2018-04-07 16:59:00 +0000
+++ src/logic/player.h	2018-04-16 08:14:47 +0000
@@ -111,6 +111,10 @@
 		get_messages()->set_message_status(id, status);
 	}
 
+	const std::set<Serial>& ships() const;
+	void add_ship(Serial ship);
+	void remove_ship(Serial ship);
+
 	const EditorGameBase& egbase() const {
 		return egbase_;
 	}
@@ -639,6 +643,7 @@
 	std::vector<bool> allowed_worker_types_;
 	std::vector<bool> allowed_building_types_;
 	Economies economies_;
+	std::set<Serial> ships_;
 	std::string name_;  // Player name
 	std::string ai_;    /**< Name of preferred AI implementation */
 

=== modified file 'src/notifications/note_ids.h'
--- src/notifications/note_ids.h	2018-04-07 16:59:00 +0000
+++ src/notifications/note_ids.h	2018-04-16 08:14:47 +0000
@@ -33,8 +33,7 @@
 	FieldTerrainChanged,
 	ProductionSiteOutOfResources,
 	TrainingSiteSoldierTrained,
-	ShipMessage,
-	ShipWindow,
+	Ship,
 	Building,
 	Economy,
 	GraphicResolutionChanged,

=== modified file 'src/scripting/lua_game.cc'
--- src/scripting/lua_game.cc	2018-04-07 16:59:00 +0000
+++ src/scripting/lua_game.cc	2018-04-16 08:14:47 +0000
@@ -672,30 +672,16 @@
 */
 int LuaPlayer::get_ships(lua_State* L) {
 	EditorGameBase& egbase = get_egbase(L);
-	const Map& map = egbase.map();
 	PlayerNumber p = (get(L, egbase)).player_number();
 	lua_newtable(L);
 	uint32_t cidx = 1;
-
-	std::set<OPtr<Ship>> found_ships;
-	for (int16_t y = 0; y < map.get_height(); ++y) {
-		for (int16_t x = 0; x < map.get_width(); ++x) {
-			FCoords f = map.get_fcoords(Coords(x, y));
-			// there are too many bobs on the map so we investigate
-			// only bobs on water
-			if (f.field->nodecaps() & MOVECAPS_SWIM) {
-				for (Bob* bob = f.field->get_first_bob(); bob; bob = bob->get_next_on_field()) {
-					if (upcast(Ship, ship, bob)) {
-						if (ship->get_owner()->player_number() == p && !found_ships.count(ship)) {
-							found_ships.insert(ship);
-							lua_pushuint32(L, cidx++);
-							LuaMaps::upcasted_map_object_to_lua(L, ship);
-							lua_rawset(L, -3);
-						}
-					}
-				}
-			}
-		}
+	for (const auto& serial : egbase.player(p).ships()) {
+		Widelands::MapObject* obj = egbase.objects().get_object(serial);
+		assert(obj->descr().type() == Widelands::MapObjectType::SHIP);
+		upcast(Widelands::Ship, ship, obj);
+		lua_pushuint32(L, cidx++);
+		LuaMaps::upcasted_map_object_to_lua(L, ship);
+		lua_rawset(L, -3);
 	}
 	return 1;
 }

=== modified file 'src/ui_basic/table.cc'
--- src/ui_basic/table.cc	2018-04-07 16:59:00 +0000
+++ src/ui_basic/table.cc	2018-04-16 08:14:47 +0000
@@ -551,6 +551,21 @@
 	layout();
 }
 
+/**
+ * Remove the given table entry if it exists.
+ */
+void Table<void*>::remove_entry(const void* const entry) {
+	EntryRecord* er = find(entry);
+	if (er != nullptr) {
+		for (uint32_t i = 0; i < entry_records_.size(); ++i) {
+			if (entry_records_[i] == er) {
+				remove(i);
+				return;
+			}
+		}
+	}
+}
+
 bool Table<void*>::sort_helper(uint32_t a, uint32_t b) {
 	if (sort_descending_) {
 		return columns_[sort_column_].compare(b, a);

=== modified file 'src/ui_basic/table.h'
--- src/ui_basic/table.h	2018-04-07 16:59:00 +0000
+++ src/ui_basic/table.h	2018-04-16 08:14:47 +0000
@@ -84,6 +84,7 @@
 
 	void sort(uint32_t lower_bound = 0, uint32_t upper_bound = std::numeric_limits<uint32_t>::max());
 	void remove(uint32_t);
+	void remove_entry(Entry);
 
 	EntryRecord& add(void* const entry, const bool select_this = false);
 
@@ -209,6 +210,7 @@
 
 	void sort(uint32_t lower_bound = 0, uint32_t upper_bound = std::numeric_limits<uint32_t>::max());
 	void remove(uint32_t);
+	void remove_entry(const void* const entry);
 
 	EntryRecord& add(void* entry = nullptr, bool select = false);
 
@@ -335,6 +337,10 @@
 	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
+	void remove_entry(Entry const* const entry) {
+		Base::remove_entry(const_cast<Entry*>(entry));
+	}
+
 	EntryRecord& add(Entry const* const entry = 0, bool const select_this = false) {
 		return Base::add(const_cast<Entry*>(entry), select_this);
 	}
@@ -365,6 +371,10 @@
 	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
+	void remove_entry(Entry const* entry) {
+		Base::remove_entry(entry);
+	}
+
 	EntryRecord& add(Entry* const entry = 0, bool const select_this = false) {
 		return Base::add(entry, select_this);
 	}
@@ -395,6 +405,10 @@
 	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
+	void remove_entry(const Entry& entry) {
+		Base::remove_entry(&const_cast<Entry&>(entry));
+	}
+
 	EntryRecord& add(const Entry& entry, bool const select_this = false) {
 		return Base::add(&const_cast<Entry&>(entry), select_this);
 	}
@@ -429,6 +443,10 @@
 	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
+	void remove_entry(Entry& entry) {
+		Base::remove_entry(&entry);
+	}
+
 	EntryRecord& add(Entry& entry, bool const select_this = false) {
 		return Base::add(&entry, select_this);
 	}
@@ -465,6 +483,10 @@
 	   : Base(parent, x, y, w, h, button_background, rowtype) {
 	}
 
+	void remove_entry(uintptr_t const entry) {
+		Base::remove_entry(reinterpret_cast<void*>(entry));
+	}
+
 	EntryRecord& add(uintptr_t const entry, bool const select_this = false) {
 		return Base::add(reinterpret_cast<void*>(entry), select_this);
 	}

=== modified file 'src/wui/CMakeLists.txt'
--- src/wui/CMakeLists.txt	2017-11-20 13:50:51 +0000
+++ src/wui/CMakeLists.txt	2018-04-16 08:14:47 +0000
@@ -227,6 +227,8 @@
     portdockwaresdisplay.h
     productionsitewindow.cc
     productionsitewindow.h
+    seafaring_statistics_menu.cc
+    seafaring_statistics_menu.h
     shipwindow.cc
     shipwindow.h
     soldiercapacitycontrol.cc

=== modified file 'src/wui/game_statistics_menu.cc'
--- src/wui/game_statistics_menu.cc	2018-04-07 16:59:00 +0000
+++ src/wui/game_statistics_menu.cc	2018-04-16 08:14:47 +0000
@@ -27,6 +27,7 @@
 #include "wui/building_statistics_menu.h"
 #include "wui/general_statistics_menu.h"
 #include "wui/interactive_player.h"
+#include "wui/seafaring_statistics_menu.h"
 #include "wui/stock_menu.h"
 #include "wui/ware_statistics_menu.h"
 
@@ -37,6 +38,7 @@
      player_(plr),
      windows_(windows),
      box_(this, 0, 0, UI::Box::Horizontal, 0, 0, 5) {
+	const bool is_seafaring = plr.egbase().mutable_map()->allows_seafaring();
 	add_button("wui/menus/menu_general_stats", "general_stats", _("General statistics"),
 	           &windows_.general_stats);
 	add_button(
@@ -44,8 +46,12 @@
 	add_button("wui/menus/menu_building_stats", "building_stats", _("Building statistics"),
 	           &windows_.building_stats);
 	add_button("wui/menus/menu_stock", "stock", _("Stock"), &windows_.stock);
+	if (is_seafaring) {
+		add_button("wui/buildings/start_expedition", "seafaring_stats", _("Seafaring Statistics"),
+					  &windows_.seafaring_stats);
+	}
 	box_.set_pos(Vector2i(10, 10));
-	box_.set_size((34 + 5) * 4, 34);
+	box_.set_size((34 + 5) * (is_seafaring ? 5 : 4), 34);
 	set_inner_size(box_.get_w() + 20, box_.get_h() + 20);
 
 	windows_.general_stats.open_window = [this] {
@@ -58,6 +64,11 @@
 		new BuildingStatisticsMenu(player_, windows_.building_stats);
 	};
 	// The stock window is defined in InteractivePlayer because of the keyboard shortcut.
+	if (is_seafaring) {
+		windows_.seafaring_stats.open_window = [this] {
+			new SeafaringStatisticsMenu(player_, windows_.seafaring_stats);
+		};
+	}
 
 	if (get_usedefaultpos())
 		center_to_parent();

=== modified file 'src/wui/interactive_gamebase.cc'
--- src/wui/interactive_gamebase.cc	2018-04-07 16:59:00 +0000
+++ src/wui/interactive_gamebase.cc	2018-04-16 08:14:47 +0000
@@ -233,20 +233,23 @@
 	for (Widelands::Bob* temp_ship : ships) {
 		if (upcast(Widelands::Ship, ship, temp_ship)) {
 			if (can_see(ship->get_owner()->player_number())) {
-				UI::UniqueWindow::Registry& registry =
-				   unique_windows().get_registry((boost::format("ship_%d") % ship->serial()).str());
-				registry.open_window = [this, &registry, ship] {
-					new ShipWindow(*this, registry, *ship);
-				};
-				registry.create();
+				show_ship_window(ship);
 				return true;
 			}
 		}
 	}
-
 	return false;
 }
 
+void InteractiveGameBase::show_ship_window(Widelands::Ship* ship) {
+	UI::UniqueWindow::Registry& registry =
+		unique_windows().get_registry((boost::format("ship_%d") % ship->serial()).str());
+	registry.open_window = [this, &registry, ship] {
+		new ShipWindow(*this, registry, ship);
+	};
+	registry.create();
+}
+
 void InteractiveGameBase::show_game_summary() {
 	if (game_summary_.window) {
 		game_summary_.window->set_visible(true);

=== modified file 'src/wui/interactive_gamebase.h'
--- src/wui/interactive_gamebase.h	2018-04-07 16:59:00 +0000
+++ src/wui/interactive_gamebase.h	2018-04-16 08:14:47 +0000
@@ -48,6 +48,7 @@
 		GeneralStatisticsMenu::Registry general_stats;
 		UI::UniqueWindow::Registry ware_stats;
 		UI::UniqueWindow::Registry stock;
+		UI::UniqueWindow::Registry seafaring_stats;
 	};
 
 	InteractiveGameBase(Widelands::Game&,
@@ -86,6 +87,7 @@
 	                                bool was_minimal);
 	UI::UniqueWindow* show_building_window(const Widelands::Coords& coords, bool avoid_fastclick);
 	bool try_show_ship_window();
+	void show_ship_window(Widelands::Ship* ship);
 	bool is_multiplayer() {
 		return multiplayer_;
 	}

=== modified file 'src/wui/interactive_player.cc'
--- src/wui/interactive_player.cc	2018-04-07 16:59:00 +0000
+++ src/wui/interactive_player.cc	2018-04-16 08:14:47 +0000
@@ -51,6 +51,7 @@
 #include "wui/game_options_menu.h"
 #include "wui/game_statistics_menu.h"
 #include "wui/general_statistics_menu.h"
+#include "wui/seafaring_statistics_menu.h"
 #include "wui/stock_menu.h"
 #include "wui/tribal_encyclopedia.h"
 #include "wui/ware_statistics_menu.h"
@@ -459,6 +460,14 @@
 			}
 			return true;
 
+		case SDLK_e:
+			if (main_windows_.seafaring_stats.window == nullptr) {
+				new SeafaringStatisticsMenu(*this, main_windows_.seafaring_stats);
+			} else {
+				main_windows_.seafaring_stats.toggle();
+			}
+			return true;
+
 		case SDLK_s:
 			if (code.mod & (KMOD_LCTRL | KMOD_RCTRL))
 				new GameMainMenuSaveGame(*this, main_windows_.savegame);

=== added file 'src/wui/seafaring_statistics_menu.cc'
--- src/wui/seafaring_statistics_menu.cc	1970-01-01 00:00:00 +0000
+++ src/wui/seafaring_statistics_menu.cc	2018-04-16 08:14:47 +0000
@@ -0,0 +1,566 @@
+/*
+ * Copyright (C) 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#include "wui/seafaring_statistics_menu.h"
+
+#include <memory>
+
+#include <boost/bind.hpp>
+#include <boost/format.hpp>
+
+#include "economy/fleet.h"
+#include "graphic/graphic.h"
+#include "logic/game.h"
+#include "logic/player.h"
+#include "logic/playercommand.h"
+#include "ui_basic/box.h"
+#include "wui/interactive_player.h"
+#include "wui/shipwindow.h"
+#include "wui/watchwindow.h"
+
+inline InteractivePlayer& SeafaringStatisticsMenu::iplayer() const {
+	return dynamic_cast<InteractivePlayer&>(*get_parent());
+}
+
+constexpr int kPadding = 5;
+constexpr int kButtonSize = 34;
+
+SeafaringStatisticsMenu::SeafaringStatisticsMenu(InteractivePlayer& plr,
+                                                 UI::UniqueWindow::Registry& registry)
+   : UI::UniqueWindow(&plr, "seafaring_statistics", &registry, 355, 375, _("Seafaring Statistics")),
+     main_box_(this, kPadding, kPadding, UI::Box::Vertical, get_inner_w(), get_inner_h(), kPadding),
+     filter_box_(
+        &main_box_, 0, 0, UI::Box::Horizontal, get_inner_w() - 2 * kPadding, kButtonSize, kPadding),
+     idle_btn_(&filter_box_,
+               "filter_ship_idle",
+               0,
+               0,
+               kButtonSize,
+               kButtonSize,
+					g_gr->images().get("images/ui_basic/but0.png"),
+               status_to_image(ShipFilterStatus::kIdle)),
+     waiting_btn_(&filter_box_,
+                  "filter_ship_waiting",
+                  0,
+                  0,
+                  kButtonSize,
+                  kButtonSize,
+						g_gr->images().get("images/ui_basic/but0.png"),
+                  status_to_image(ShipFilterStatus::kExpeditionWaiting)),
+     scouting_btn_(&filter_box_,
+                   "filter_ship_scouting",
+                   0,
+                   0,
+                   kButtonSize,
+                   kButtonSize,
+						 g_gr->images().get("images/ui_basic/but0.png"),
+                   status_to_image(ShipFilterStatus::kExpeditionScouting)),
+     portspace_btn_(&filter_box_,
+                    "filter_ship_portspace",
+                    0,
+                    0,
+                    kButtonSize,
+                    kButtonSize,
+						  g_gr->images().get("images/ui_basic/but0.png"),
+                    status_to_image(ShipFilterStatus::kExpeditionPortspaceFound)),
+     shipping_btn_(&filter_box_,
+                   "filter_ship_transporting",
+                   0,
+                   0,
+                   kButtonSize,
+                   kButtonSize,
+						 g_gr->images().get("images/ui_basic/but0.png"),
+                   status_to_image(ShipFilterStatus::kShipping)),
+     ship_filter_(ShipFilterStatus::kAll),
+     navigation_box_(
+        &main_box_, 0, 0, UI::Box::Horizontal, get_inner_w() - 2 * kPadding, kButtonSize, kPadding),
+     watchbtn_(&navigation_box_,
+               "seafaring_stats_watch_button",
+               0,
+               0,
+               kButtonSize,
+               kButtonSize,
+					g_gr->images().get("images/ui_basic/but2.png"),
+               g_gr->images().get("images/wui/menus/menu_watch_follow.png"),
+               (boost::format(_("%1% (Hotkey: %2%)"))
+                %
+					 /** TRANSLATORS: Tooltip in the seafaring statistics window */
+                _("Watch the selected ship") % pgettext("hotkey", "W"))
+                  .str()),
+     openwindowbtn_(&navigation_box_,
+                    "seafaring_stats_watch_button",
+                    0,
+                    0,
+                    kButtonSize,
+                    kButtonSize,
+						  g_gr->images().get("images/ui_basic/but2.png"),
+                    g_gr->images().get("images/ui_basic/fsel.png"),
+                    (boost::format(_("%1% (Hotkey: %2%)"))
+                     %
+							/** TRANSLATORS: Tooltip in the seafaring statistics window */
+                     _("Go to the selected ship and open its window") % pgettext("hotkey", "O"))
+                       .str()),
+     centerviewbtn_(&navigation_box_,
+                    "seafaring_stats_center_main_mapview_button",
+                    0,
+                    0,
+                    kButtonSize,
+                    kButtonSize,
+						  g_gr->images().get("images/ui_basic/but2.png"),
+                    g_gr->images().get("images/wui/ship/menu_ship_goto.png"),
+                    (boost::format(_("%1% (Hotkey: %2%)"))
+                     %
+							/** TRANSLATORS: Tooltip in the seafaring statistics window */
+                     _("Center the map on the selected ship") % pgettext("hotkey", "G"))
+                       .str()),
+     table_(&main_box_,
+            0,
+            0,
+            get_inner_w() - 2 * kPadding,
+            100,
+            g_gr->images().get("images/ui_basic/but1.png")) {
+
+	const Widelands::TribeDescr& tribe = iplayer().player().tribe();
+	colony_icon_ = tribe.get_worker_descr(tribe.builder())->icon();
+
+	// Buttons for ship states
+	main_box_.add(&filter_box_, UI::Box::Resizing::kFullSize);
+	filter_box_.add(&idle_btn_);
+	filter_box_.add(&shipping_btn_);
+	filter_box_.add(&waiting_btn_);
+	filter_box_.add(&scouting_btn_);
+	filter_box_.add(&portspace_btn_);
+
+	main_box_.add(&table_, UI::Box::Resizing::kExpandBoth);
+
+	// Navigation buttons
+	main_box_.add(&navigation_box_, UI::Box::Resizing::kFullSize);
+	navigation_box_.add(&watchbtn_);
+	navigation_box_.add_inf_space();
+	navigation_box_.add(&openwindowbtn_);
+	navigation_box_.add(&centerviewbtn_);
+	main_box_.set_size(get_inner_w() - 2 * kPadding, get_inner_h() - 2 * kPadding);
+
+	// Configure actions
+	idle_btn_.sigclicked.connect(
+	   boost::bind(&SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kIdle));
+	shipping_btn_.sigclicked.connect(
+	   boost::bind(&SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kShipping));
+	waiting_btn_.sigclicked.connect(boost::bind(
+	   &SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kExpeditionWaiting));
+	scouting_btn_.sigclicked.connect(boost::bind(
+	   &SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kExpeditionScouting));
+	portspace_btn_.sigclicked.connect(boost::bind(
+	   &SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kExpeditionPortspaceFound));
+	ship_filter_ = ShipFilterStatus::kAll;
+	set_filter_ships_tooltips();
+
+	watchbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::watch_ship, this));
+	openwindowbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::open_ship_window, this));
+	centerviewbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::center_view, this));
+
+	// Configure table
+	table_.selected.connect(boost::bind(&SeafaringStatisticsMenu::selected, this));
+	table_.double_clicked.connect(boost::bind(&SeafaringStatisticsMenu::double_clicked, this));
+	table_.add_column(
+	   0, pgettext("ship", "Name"), "", UI::Align::kLeft, UI::TableColumnType::kFlexible);
+	table_.add_column(200, pgettext("ship", "Status"));
+	table_.set_sort_column(ColName);
+	fill_table();
+
+	set_can_focus(true);
+	set_thinks(false);
+	table_.focus();
+
+	shipnotes_subscriber_ =
+	   Notifications::subscribe<Widelands::NoteShip>([this](const Widelands::NoteShip& note) {
+			if (iplayer().get_player() == note.ship->get_owner()) {
+				switch (note.action) {
+				case Widelands::NoteShip::Action::kDestinationChanged:
+				case Widelands::NoteShip::Action::kWaitingForCommand:
+				case Widelands::NoteShip::Action::kGained:
+					update_ship(*note.ship);
+					break;
+				case Widelands::NoteShip::Action::kLost:
+					remove_ship(note.ship->serial());
+					break;
+				default:
+					NEVER_HERE();
+				}
+			}
+		});
+}
+
+const std::string
+SeafaringStatisticsMenu::status_to_string(SeafaringStatisticsMenu::ShipFilterStatus status) const {
+	switch (status) {
+	case SeafaringStatisticsMenu::ShipFilterStatus::kIdle:
+		return pgettext("ship_state", "Idle");
+	case SeafaringStatisticsMenu::ShipFilterStatus::kShipping:
+		return pgettext("ship_state", "Shipping");
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionWaiting:
+		return pgettext("ship_state", "Waiting");
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionScouting:
+		return pgettext("ship_state", "Scouting");
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionPortspaceFound:
+		return pgettext("ship_state", "Port Space Found");
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionColonizing:
+		return pgettext("ship_state", "Founding a Colony");
+	case SeafaringStatisticsMenu::ShipFilterStatus::kAll:
+		return "All";  // The user shouldn't see this, so we don't localize
+	default:
+		NEVER_HERE();
+	}
+}
+
+const Image*
+SeafaringStatisticsMenu::status_to_image(SeafaringStatisticsMenu::ShipFilterStatus status) const {
+	std::string filename = "";
+	switch (status) {
+	case SeafaringStatisticsMenu::ShipFilterStatus::kIdle:
+		filename = "images/wui/stats/ship_stats_idle.png";
+		break;
+	case SeafaringStatisticsMenu::ShipFilterStatus::kShipping:
+		filename = "images/wui/stats/ship_stats_shipping.png";
+		break;
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionWaiting:
+		filename = "images/wui/buildings/start_expedition.png";
+		break;
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionScouting:
+		filename = "images/wui/ship/ship_explore_island_cw.png";
+		break;
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionPortspaceFound:
+		filename = "images/wui/ship/ship_construct_port_space.png";
+		break;
+	case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionColonizing:
+		return colony_icon_;
+	case SeafaringStatisticsMenu::ShipFilterStatus::kAll:
+		filename = "images/wui/ship/ship_scout_ne.png";
+		break;
+	default:
+		NEVER_HERE();
+	}
+	return g_gr->images().get(filename);
+}
+
+const SeafaringStatisticsMenu::ShipInfo*
+SeafaringStatisticsMenu::create_shipinfo(const Widelands::Ship& ship) const {
+	if (&ship == nullptr) {
+		return new ShipInfo();
+	}
+	const Widelands::Ship::ShipStates state = ship.get_ship_state();
+	ShipFilterStatus status = ShipFilterStatus::kAll;
+	switch (state) {
+	case Widelands::Ship::ShipStates::kTransport:
+		if (ship.get_destination(iplayer().game()) != nullptr) {
+			status = ShipFilterStatus::kShipping;
+		} else {
+			status = ShipFilterStatus::kIdle;
+		}
+		break;
+	case Widelands::Ship::ShipStates::kExpeditionWaiting:
+		status = ShipFilterStatus::kExpeditionWaiting;
+		break;
+	case Widelands::Ship::ShipStates::kExpeditionScouting:
+		status = ShipFilterStatus::kExpeditionScouting;
+		break;
+	case Widelands::Ship::ShipStates::kExpeditionPortspaceFound:
+		status = ShipFilterStatus::kExpeditionPortspaceFound;
+		break;
+	case Widelands::Ship::ShipStates::kExpeditionColonizing:
+		status = ShipFilterStatus::kExpeditionColonizing;
+		break;
+	case Widelands::Ship::ShipStates::kSinkRequest:
+	case Widelands::Ship::ShipStates::kSinkAnimation:
+		status = ShipFilterStatus::kAll;
+	}
+	return new ShipInfo(ship.get_shipname(), status, ship.serial());
+}
+
+void SeafaringStatisticsMenu::set_entry_record(UI::Table<uintptr_t>::EntryRecord* er,
+                                               const ShipInfo& info) {
+	if (info.status != ShipFilterStatus::kAll) {
+		er->set_string(ColName, info.name);
+		er->set_picture(ColStatus, status_to_image(info.status), status_to_string(info.status));
+	}
+}
+
+Widelands::Ship* SeafaringStatisticsMenu::serial_to_ship(Widelands::Serial serial) const {
+	Widelands::MapObject* obj = iplayer().game().objects().get_object(serial);
+	assert(obj->descr().type() == Widelands::MapObjectType::SHIP);
+	upcast(Widelands::Ship, ship, obj);
+	return ship;
+}
+
+void SeafaringStatisticsMenu::update_ship(const Widelands::Ship& ship) {
+	assert(iplayer().get_player() == ship.get_owner());
+	const ShipInfo* info = create_shipinfo(ship);
+	// Remove ships that don't satisfy the filter
+	if (ship_filter_ != ShipFilterStatus::kAll && !satisfies_filter(*info, ship_filter_)) {
+		remove_ship(info->serial);
+		return;
+	}
+	// Try to find the ship in the table
+	if (data_.count(info->serial) == 1) {
+		const ShipInfo* old_info = data_[info->serial].get();
+		if (info->status != old_info->status) {
+			// The status has changed - we need an update
+			data_[info->serial] = std::unique_ptr<const ShipInfo>(info);
+			UI::Table<uintptr_t>::EntryRecord* er = table_.find(info->serial);
+			set_entry_record(er, *info);
+		}
+	} else {
+		// This is a new ship or it was filtered away before
+		data_.insert(std::make_pair(info->serial, std::unique_ptr<const ShipInfo>(info)));
+		UI::Table<uintptr_t>::EntryRecord& er = table_.add(info->serial);
+		set_entry_record(&er, *info);
+	}
+	table_.sort();
+	set_buttons_enabled();
+}
+
+void SeafaringStatisticsMenu::remove_ship(Widelands::Serial serial) {
+	if (data_.count(serial) == 1) {
+		table_.remove_entry(serial);
+		data_.erase(data_.find(serial));
+		if (!table_.empty() && !table_.has_selection()) {
+			table_.select(0);
+		}
+		set_buttons_enabled();
+	}
+}
+
+void SeafaringStatisticsMenu::update_entry_record(UI::Table<uintptr_t>::EntryRecord& er,
+                                                  const ShipInfo& info) {
+	er.set_picture(ColStatus, status_to_image(info.status), status_to_string(info.status));
+}
+
+void SeafaringStatisticsMenu::selected() {
+	set_buttons_enabled();
+}
+
+void SeafaringStatisticsMenu::double_clicked() {
+	if (table_.has_selection()) {
+		center_view();
+	}
+}
+
+void SeafaringStatisticsMenu::set_buttons_enabled() {
+	centerviewbtn_.set_enabled(table_.has_selection());
+	openwindowbtn_.set_enabled(table_.has_selection());
+	watchbtn_.set_enabled(table_.has_selection());
+}
+
+bool SeafaringStatisticsMenu::handle_key(bool down, SDL_Keysym code) {
+	if (down) {
+		switch (code.sym) {
+		// Don't forget to change the tooltips if any of these get reassigned
+		case SDLK_g:
+			center_view();
+			return true;
+		case SDLK_o:
+			open_ship_window();
+			return true;
+		case SDLK_w:
+			watch_ship();
+			return true;
+		case SDLK_0:
+			if (code.mod & KMOD_ALT) {
+				filter_ships(ShipFilterStatus::kAll);
+				return true;
+			}
+			return false;
+		case SDLK_1:
+			if (code.mod & KMOD_ALT) {
+				filter_ships(ShipFilterStatus::kIdle);
+				return true;
+			}
+			return false;
+		case SDLK_2:
+			if (code.mod & KMOD_ALT) {
+				filter_ships(ShipFilterStatus::kShipping);
+				return true;
+			}
+			return false;
+		case SDLK_3:
+			if (code.mod & KMOD_ALT) {
+				filter_ships(ShipFilterStatus::kExpeditionWaiting);
+				return true;
+			}
+			return false;
+		case SDLK_4:
+			if (code.mod & KMOD_ALT) {
+				filter_ships(ShipFilterStatus::kExpeditionScouting);
+				return true;
+			}
+			return false;
+		case SDLK_5:
+			if (code.mod & KMOD_ALT) {
+				filter_ships(ShipFilterStatus::kExpeditionPortspaceFound);
+				return true;
+			}
+			return false;
+		case SDL_SCANCODE_KP_PERIOD:
+		case SDLK_KP_PERIOD:
+			if (code.mod & KMOD_NUM)
+				break;
+		/* no break */
+		default:
+			break;  // not handled
+		}
+	}
+
+	return table_.handle_key(down, code);
+}
+
+void SeafaringStatisticsMenu::center_view() {
+	if (table_.has_selection()) {
+		Widelands::Ship* ship = serial_to_ship(table_.get_selected());
+		iplayer().map_view()->scroll_to_field(ship->get_position(), MapView::Transition::Smooth);
+	}
+}
+
+void SeafaringStatisticsMenu::watch_ship() {
+	if (table_.has_selection()) {
+		Widelands::Ship* ship = serial_to_ship(table_.get_selected());
+		WatchWindow* window = show_watch_window(iplayer(), ship->get_position());
+		window->follow(ship);
+	}
+}
+
+void SeafaringStatisticsMenu::open_ship_window() {
+	if (table_.has_selection()) {
+		center_view();
+		Widelands::Ship* ship = serial_to_ship(table_.get_selected());
+		iplayer().show_ship_window(ship);
+	}
+}
+
+void SeafaringStatisticsMenu::filter_ships(ShipFilterStatus status) {
+	switch (status) {
+	case ShipFilterStatus::kExpeditionWaiting:
+		toggle_filter_ships_button(waiting_btn_, status);
+		break;
+	case ShipFilterStatus::kExpeditionScouting:
+		toggle_filter_ships_button(scouting_btn_, status);
+		break;
+	// We're grouping the "colonizing" status with the port space.
+	case ShipFilterStatus::kExpeditionColonizing:
+	case ShipFilterStatus::kExpeditionPortspaceFound:
+		toggle_filter_ships_button(portspace_btn_, status);
+		break;
+	case ShipFilterStatus::kShipping:
+		toggle_filter_ships_button(shipping_btn_, status);
+		break;
+	case ShipFilterStatus::kIdle:
+		toggle_filter_ships_button(idle_btn_, status);
+		break;
+	case ShipFilterStatus::kAll:
+		set_filter_ships_tooltips();
+		ship_filter_ = ShipFilterStatus::kAll;
+		waiting_btn_.set_perm_pressed(false);
+		scouting_btn_.set_perm_pressed(false);
+		portspace_btn_.set_perm_pressed(false);
+		shipping_btn_.set_perm_pressed(false);
+		idle_btn_.set_perm_pressed(false);
+		break;
+	}
+	fill_table();
+}
+
+void SeafaringStatisticsMenu::toggle_filter_ships_button(UI::Button& button,
+                                                         ShipFilterStatus status) {
+	set_filter_ships_tooltips();
+	if (button.style() == UI::Button::Style::kPermpressed) {
+		button.set_perm_pressed(false);
+		ship_filter_ = ShipFilterStatus::kAll;
+	} else {
+		waiting_btn_.set_perm_pressed(false);
+		scouting_btn_.set_perm_pressed(false);
+		portspace_btn_.set_perm_pressed(false);
+		shipping_btn_.set_perm_pressed(false);
+		idle_btn_.set_perm_pressed(false);
+		button.set_perm_pressed(true);
+		ship_filter_ = status;
+
+		/** TRANSLATORS: %1% is a tooltip, %2% is the corresponding hotkey */
+		button.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+		                    /** TRANSLATORS: Tooltip in the messages window */
+		                    % _("Show all ships") % pgettext("hotkey", "Alt + 0"))
+		                      .str());
+	}
+}
+
+void SeafaringStatisticsMenu::set_filter_ships_tooltips() {
+
+	idle_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                       /** TRANSLATORS: Tooltip in the messages window */
+	                       % _("Show idle ships") % pgettext("hotkey", "Alt + 1"))
+	                         .str());
+	shipping_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                           /** TRANSLATORS: Tooltip in the messages window */
+	                           % _("Show ships shipping wares and workers") %
+	                           pgettext("hotkey", "Alt + 2"))
+	                             .str());
+	waiting_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                          /** TRANSLATORS: Tooltip in the messages window */
+	                          % _("Show waiting expeditions") % pgettext("hotkey", "Alt + 3"))
+	                            .str());
+	scouting_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                           /** TRANSLATORS: Tooltip in the messages window */
+	                           % _("Show scouting expeditions") % pgettext("hotkey", "Alt + 4"))
+	                             .str());
+	portspace_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
+	                            /** TRANSLATORS: Tooltip in the messages window */
+	                            % _("Show colonizing expeditions and expeditions with port space found") %
+	                            pgettext("hotkey", "Alt + 5"))
+	                              .str());
+}
+
+bool SeafaringStatisticsMenu::satisfies_filter(const ShipInfo& info, ShipFilterStatus filter) {
+	return filter == info.status ||
+			(filter == ShipFilterStatus::kExpeditionPortspaceFound
+			 && info.status == ShipFilterStatus::kExpeditionColonizing);
+}
+
+void SeafaringStatisticsMenu::fill_table() {
+	const Widelands::Serial last_selection = table_.has_selection() ? table_.get_selected() : Widelands::INVALID_INDEX;
+	table_.clear();
+	data_.clear();
+	set_buttons_enabled();
+	for (const auto& serial : iplayer().player().ships()) {
+		Widelands::Ship* ship = serial_to_ship(serial);
+		assert(iplayer().get_player() == ship->get_owner());
+		const ShipInfo* info = create_shipinfo(*ship);
+		if (info->status != ShipFilterStatus::kAll) {
+			if (ship_filter_ == ShipFilterStatus::kAll || satisfies_filter(*info, ship_filter_)) {
+				data_.insert(std::make_pair(serial, std::unique_ptr<const ShipInfo>(info)));
+				UI::Table<uintptr_t const>::EntryRecord& er = table_.add(serial, serial == last_selection);
+				set_entry_record(&er, *info);
+			}
+		}
+	}
+
+	if (!table_.empty()) {
+		table_.sort();
+		if (!table_.has_selection()) {
+			table_.select(0);
+		}
+	}
+}

=== added file 'src/wui/seafaring_statistics_menu.h'
--- src/wui/seafaring_statistics_menu.h	1970-01-01 00:00:00 +0000
+++ src/wui/seafaring_statistics_menu.h	2018-04-16 08:14:47 +0000
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_WUI_SEAFARING_STATISTICS_MENU_H
+#define WL_WUI_SEAFARING_STATISTICS_MENU_H
+
+#include <memory>
+#include <unordered_map>
+
+#include "base/i18n.h"
+#include "logic/map_objects/tribes/ship.h"
+#include "notifications/notifications.h"
+#include "ui_basic/box.h"
+#include "ui_basic/button.h"
+#include "ui_basic/table.h"
+#include "ui_basic/unique_window.h"
+
+class InteractivePlayer;
+
+/// Shows a list of the ships owned by the interactive player with filtering and navigation options.
+struct SeafaringStatisticsMenu : public UI::UniqueWindow {
+	SeafaringStatisticsMenu(InteractivePlayer&, UI::UniqueWindow::Registry&);
+
+private:
+	/// For identifying the columns in the table.
+	enum Cols { ColName, ColStatus };
+	/**
+	 * A list of ship status that we can filter for. This differs a bit from that the Widelands::Ship
+	 * class has, so we define our own.
+	 * */
+	enum class ShipFilterStatus {
+		kIdle,
+		kShipping,
+		kExpeditionWaiting,
+		kExpeditionScouting,
+		kExpeditionPortspaceFound,
+		kExpeditionColonizing,
+		kAll
+	};
+
+	/// Returns the localized strings that we use to display the 'status' in the table.
+	const std::string status_to_string(ShipFilterStatus status) const;
+	/// Returns the icon that we use to represent the 'status' in the table and on the filter
+	/// buttons.
+	const Image* status_to_image(ShipFilterStatus status) const;
+
+	/// The dataset that we need to display ships in the table.
+	struct ShipInfo {
+		ShipInfo(const std::string& init_name,
+		         const ShipFilterStatus init_status,
+		         const Widelands::Serial init_serial)
+		   : name(init_name), status(init_status), serial(init_serial) {
+		}
+		ShipInfo() : ShipInfo("", ShipFilterStatus::kAll, 0) {
+		}
+		ShipInfo(const ShipInfo& other) : ShipInfo(other.name, other.status, other.serial) {
+		}
+		bool operator==(const ShipInfo& other) const {
+			return serial == other.serial;
+		}
+		bool operator<(const ShipInfo& other) const {
+			return serial < other.serial;
+		}
+
+		const std::string name;
+		ShipFilterStatus status;
+		const Widelands::Serial serial;
+	};
+
+	/// Creates our dataset from a 'ship'. Make sure to take ownership of the result.
+	const ShipInfo* create_shipinfo(const Widelands::Ship& ship) const;
+	/// Uses the 'serial' to identify and get a ship from the game.
+	Widelands::Ship* serial_to_ship(Widelands::Serial serial) const;
+
+	/// Convenience function to upcast the panel's parent.
+	InteractivePlayer& iplayer() const;
+
+	/// A ship was selected, so enable the navigation buttons.
+	void selected();
+	/// A ship was double clicked. Centers main view on ship.
+	void double_clicked();
+	/// Handle filter and navigation hotkeys
+	bool handle_key(bool down, SDL_Keysym code) override;
+
+	/// Enables the navigation buttons if a ship is selected, disables them otherwise.
+	void set_buttons_enabled();
+	/// Center the mapview on the currently selected ship.
+	void center_view();
+	/// Follow the selected ship in a watch window.
+	void watch_ship();
+	/// Center the mapview on the currently selected ship and open its window.
+	void open_ship_window();
+
+	/**
+	 * Updates the status for the ship. If the ship is new and satisfies the 'ship_filter_',
+	 * adds it to the table and data. If it doesn't satisfy the 'ship_filter_', removes the ship
+	 * instead.
+	 * */
+	void update_ship(const Widelands::Ship&);
+	/// If we listed the ship, remove it from table and data.
+	void remove_ship(Widelands::Serial serial);
+	/// Sets the contents for the entry record in the table.
+	void set_entry_record(UI::Table<uintptr_t>::EntryRecord*, const ShipInfo& info);
+	/// Updates the ship status display in the table.
+	void update_entry_record(UI::Table<uintptr_t>::EntryRecord& er, const ShipInfo&);
+	/// Rebuilds data and table with all ships that satisfy the current 'ship_filter_'.
+	void fill_table();
+
+	/// Show only the ships that have the given status. Toggle the appropriate buttons.
+	void filter_ships(ShipFilterStatus status);
+	/// Helper for filter_ships
+	void toggle_filter_ships_button(UI::Button&, ShipFilterStatus);
+	/// Helper for filter_ships
+	void set_filter_ships_tooltips();
+
+	/// We group colonizing status with port space found. Anything else needs to have an identical status.
+	bool satisfies_filter(const ShipInfo& info, ShipFilterStatus filter);
+
+	const Image* colony_icon_;
+	UI::Box main_box_;
+	// Buttons for ship states
+	UI::Box filter_box_;
+	UI::Button idle_btn_;
+	UI::Button waiting_btn_;
+	UI::Button scouting_btn_;
+	UI::Button portspace_btn_;
+	UI::Button shipping_btn_;
+	ShipFilterStatus ship_filter_;
+	// Navigation buttons
+	UI::Box navigation_box_;
+	UI::Button watchbtn_;
+	UI::Button openwindowbtn_;
+	UI::Button centerviewbtn_;
+
+	// Data
+	UI::Table<uintptr_t> table_;
+	std::unordered_map<Widelands::Serial, std::unique_ptr<const ShipInfo>> data_;
+
+	std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> shipnotes_subscriber_;
+};
+
+#endif  // end of include guard: WL_WUI_SEAFARING_STATISTICS_MENU_H

=== modified file 'src/wui/shipwindow.cc'
--- src/wui/shipwindow.cc	2018-04-07 16:59:00 +0000
+++ src/wui/shipwindow.cc	2018-04-16 08:14:47 +0000
@@ -46,25 +46,25 @@
 static const char pic_scout_e[] = "images/wui/ship/ship_scout_e.png";
 static const char pic_scout_sw[] = "images/wui/ship/ship_scout_sw.png";
 static const char pic_scout_se[] = "images/wui/ship/ship_scout_se.png";
-static const char pic_construct_port[] = "images/wui/editor/fsel_editor_set_port_space.png";
+static const char pic_construct_port[] = "images/wui/ship/ship_construct_port_space.png";
 
 constexpr int kPadding = 5;
 }  // namespace
 
 using namespace Widelands;
 
-ShipWindow::ShipWindow(InteractiveGameBase& igb, UniqueWindow::Registry& reg, Ship& ship)
-   : UniqueWindow(&igb, "shipwindow", &reg, 0, 0, ship.get_shipname()),
+ShipWindow::ShipWindow(InteractiveGameBase& igb, UniqueWindow::Registry& reg, Ship* ship)
+   : UniqueWindow(&igb, "shipwindow", &reg, 0, 0, ship->get_shipname()),
      igbase_(igb),
      ship_(ship),
      vbox_(this, 0, 0, UI::Box::Vertical),
      navigation_box_(&vbox_, 0, 0, UI::Box::Vertical),
      navigation_box_height_(0) {
 	vbox_.set_inner_spacing(kPadding);
-	assert(ship_.get_owner());
+	assert(ship->get_owner());
 
-	display_ = new ItemWaresDisplay(&vbox_, *ship_.get_owner());
-	display_->set_capacity(ship_.descr().get_capacity());
+	display_ = new ItemWaresDisplay(&vbox_, ship->owner());
+	display_->set_capacity(ship->descr().get_capacity());
 	vbox_.add(display_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
 
 	// Expedition buttons
@@ -158,30 +158,26 @@
 	move_out_of_the_way();
 	warp_mouse_to_fastclick_panel();
 
-	shipnotes_subscriber_ = Notifications::subscribe<Widelands::NoteShipWindow>([this](
-	   const Widelands::NoteShipWindow& note) {
-		if (note.serial == ship_.serial()) {
+	shipnotes_subscriber_ = Notifications::subscribe<Widelands::NoteShip>([this](
+	   const Widelands::NoteShip& note) {
+		if (note.ship->serial() == ship_.serial()) {
 			switch (note.action) {
 			// Unable to cancel the expedition
-			case Widelands::NoteShipWindow::Action::kNoPortLeft:
-				if (upcast(InteractiveGameBase, igamebase, ship_.get_owner()->egbase().get_ibase())) {
-					if (igamebase->can_act(ship_.get_owner()->player_number())) {
-						UI::WLMessageBox messagebox(
-						   get_parent(),
-						   /** TRANSLATORS: Window label when an expedition can't be canceled */
-						   _("Cancel Expedition"), _("This expedition can’t be canceled, because the "
-						                             "ship has no port to return to."),
-						   UI::WLMessageBox::MBoxType::kOk);
-						messagebox.run<UI::Panel::Returncodes>();
-					}
-				}
+			case Widelands::NoteShip::Action::kNoPortLeft:
+				no_port_error_message();
 				break;
 			// The ship is no more
-			case Widelands::NoteShipWindow::Action::kClose:
+			case Widelands::NoteShip::Action::kLost:
 				// Stop this from thinking to avoid segfaults
 				set_thinks(false);
 				die();
 				break;
+			// If the ship state has changed, e.g. expedition started or scouting direction changed,
+			// think() will take care of it.
+			case Widelands::NoteShip::Action::kDestinationChanged:
+			case Widelands::NoteShip::Action::kWaitingForCommand:
+			case Widelands::NoteShip::Action::kGained:
+				break;
 			}
 		}
 	});
@@ -195,10 +191,14 @@
 }
 
 void ShipWindow::set_button_visibility() {
-	if (navigation_box_.is_visible() != ship_.state_is_expedition()) {
-		navigation_box_.set_visible(ship_.state_is_expedition());
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	if (navigation_box_.is_visible() != ship->state_is_expedition()) {
+		navigation_box_.set_visible(ship->state_is_expedition());
 		navigation_box_.set_desired_size(
-		   navigation_box_.get_w(), ship_.state_is_expedition() ? navigation_box_height_ : 0);
+		   navigation_box_.get_w(), ship->state_is_expedition() ? navigation_box_height_ : 0);
 		layout();
 	}
 	if (btn_cancel_expedition_->is_visible() != btn_cancel_expedition_->enabled()) {
@@ -206,19 +206,41 @@
 	}
 }
 
+void ShipWindow::no_port_error_message() {
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	if (upcast(InteractiveGameBase, igamebase, ship->get_owner()->egbase().get_ibase())) {
+		if (igamebase->can_act(ship->owner().player_number())) {
+			UI::WLMessageBox messagebox(
+			   get_parent(),
+			   /** TRANSLATORS: Window label when an expedition can't be canceled */
+			   _("Cancel Expedition"), _("This expedition can’t be canceled, because the "
+			                             "ship has no port to return to."),
+			   UI::WLMessageBox::MBoxType::kOk);
+			messagebox.run<UI::Panel::Returncodes>();
+		}
+	}
+}
+
 void ShipWindow::think() {
 	UI::Window::think();
-	InteractiveBase* ib = ship_.get_owner()->egbase().get_ibase();
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	InteractiveBase* ib = ship->get_owner()->egbase().get_ibase();
 	bool can_act = false;
 	if (upcast(InteractiveGameBase, igb, ib))
-		can_act = igb->can_act(ship_.get_owner()->player_number());
+		can_act = igb->can_act(ship->owner().player_number());
 
-	btn_destination_->set_enabled(ship_.get_destination(igbase_.egbase()));
+	btn_destination_->set_enabled(ship->get_destination(igbase_.egbase()));
 	btn_sink_->set_enabled(can_act);
 
 	display_->clear();
-	for (uint32_t idx = 0; idx < ship_.get_nritems(); ++idx) {
-		Widelands::ShippingItem item = ship_.get_item(idx);
+	for (uint32_t idx = 0; idx < ship->get_nritems(); ++idx) {
+		Widelands::ShippingItem item = ship->get_item(idx);
 		Widelands::WareInstance* ware;
 		Widelands::Worker* worker;
 		item.get(igbase_.egbase(), &ware, &worker);
@@ -231,8 +253,8 @@
 		}
 	}
 
-	Ship::ShipStates state = ship_.get_ship_state();
-	if (ship_.state_is_expedition()) {
+	Ship::ShipStates state = ship->get_ship_state();
+	if (ship->state_is_expedition()) {
 		/* The following rules apply:
 		 * - The "construct port" button is only active, if the ship is waiting for commands and found
 		 * a port
@@ -249,9 +271,9 @@
 		bool coast_nearby = false;
 		for (Direction dir = 1; dir <= LAST_DIRECTION; ++dir) {
 			// NOTE buttons are saved in the format DIRECTION - 1
-			btn_scout_[dir - 1]->set_enabled(can_act && ship_.exp_dir_swimmable(dir) &&
+			btn_scout_[dir - 1]->set_enabled(can_act && ship->exp_dir_swimmable(dir) &&
 			                                 (state != Ship::ShipStates::kExpeditionColonizing));
-			coast_nearby |= !ship_.exp_dir_swimmable(dir);
+			coast_nearby |= !ship->exp_dir_swimmable(dir);
 		}
 		btn_explore_island_cw_->set_enabled(can_act && coast_nearby &&
 		                                    (state != Ship::ShipStates::kExpeditionColonizing));
@@ -259,7 +281,7 @@
 		                                     (state != Ship::ShipStates::kExpeditionColonizing));
 		btn_sink_->set_enabled(can_act && (state != Ship::ShipStates::kExpeditionColonizing));
 	}
-	btn_cancel_expedition_->set_enabled(ship_.state_is_expedition() && can_act &&
+	btn_cancel_expedition_->set_enabled(ship->state_is_expedition() && can_act &&
 	                                    (state != Ship::ShipStates::kExpeditionColonizing));
 	// Expedition specific buttons
 	set_button_visibility();
@@ -279,12 +301,20 @@
 
 /// Move the main view towards the current ship location
 void ShipWindow::act_goto() {
-	igbase_.map_view()->scroll_to_field(ship_.get_position(), MapView::Transition::Smooth);
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	igbase_.map_view()->scroll_to_field(ship->get_position(), MapView::Transition::Smooth);
 }
 
 /// Move the main view towards the current destination of the ship
 void ShipWindow::act_destination() {
-	if (PortDock* destination = ship_.get_destination(igbase_.egbase())) {
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	if (PortDock* destination = ship->get_destination(igbase_.egbase())) {
 		igbase_.map_view()->scroll_to_field(
 		   destination->get_warehouse()->get_position(), MapView::Transition::Smooth);
 	}
@@ -292,53 +322,77 @@
 
 /// Sink the ship if confirmed
 void ShipWindow::act_sink() {
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
 	if (SDL_GetModState() & KMOD_CTRL) {
-		igbase_.game().send_player_sink_ship(ship_);
+		igbase_.game().send_player_sink_ship(*ship);
 	} else {
-		show_ship_sink_confirm(dynamic_cast<InteractivePlayer&>(igbase_), ship_);
+		show_ship_sink_confirm(dynamic_cast<InteractivePlayer&>(igbase_), *ship);
 	}
 }
 
 /// Show debug info
 void ShipWindow::act_debug() {
-	show_mapobject_debug(igbase_, ship_);
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	show_mapobject_debug(igbase_, *ship);
 }
 
 /// Cancel expedition if confirmed
 void ShipWindow::act_cancel_expedition() {
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
 	if (SDL_GetModState() & KMOD_CTRL) {
-		igbase_.game().send_player_cancel_expedition_ship(ship_);
+		igbase_.game().send_player_cancel_expedition_ship(*ship);
 	} else {
-		show_ship_cancel_expedition_confirm(dynamic_cast<InteractivePlayer&>(igbase_), ship_);
+		show_ship_cancel_expedition_confirm(dynamic_cast<InteractivePlayer&>(igbase_), *ship);
 	}
 }
 
 /// Sends a player command to the ship to scout towards a specific direction
 void ShipWindow::act_scout_towards(WalkingDir direction) {
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
 	// ignore request if the direction is not swimmable at all
-	if (!ship_.exp_dir_swimmable(static_cast<Direction>(direction)))
+	if (!ship->exp_dir_swimmable(static_cast<Direction>(direction)))
 		return;
-	igbase_.game().send_player_ship_scouting_direction(ship_, direction);
+	igbase_.game().send_player_ship_scouting_direction(*ship, direction);
 }
 
 /// Constructs a port at the port build space in vision range
 void ShipWindow::act_construct_port() {
-	if (ship_.exp_port_spaces().empty())
-		return;
-	igbase_.game().send_player_ship_construct_port(ship_, ship_.exp_port_spaces().front());
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
+	if (ship->exp_port_spaces().empty())
+		return;
+	igbase_.game().send_player_ship_construct_port(*ship, ship->exp_port_spaces().front());
 }
 
 /// Explores the island cw or ccw
 void ShipWindow::act_explore_island(IslandExploreDirection direction) {
+	Widelands::Ship* ship = ship_.get(igbase_.egbase());
+	if (ship == nullptr) {
+		return;
+	}
 	bool coast_nearby = false;
 	bool moveable = false;
 	for (Direction dir = 1; (dir <= LAST_DIRECTION) && (!coast_nearby || !moveable); ++dir) {
-		if (!ship_.exp_dir_swimmable(dir))
+		if (!ship->exp_dir_swimmable(dir))
 			coast_nearby = true;
 		else
 			moveable = true;
 	}
 	if (!coast_nearby || !moveable)
 		return;
-	igbase_.game().send_player_ship_explore_island(ship_, direction);
+	igbase_.game().send_player_ship_explore_island(*ship, direction);
 }

=== modified file 'src/wui/shipwindow.h'
--- src/wui/shipwindow.h	2018-04-07 16:59:00 +0000
+++ src/wui/shipwindow.h	2018-04-16 08:14:47 +0000
@@ -36,7 +36,7 @@
  */
 class ShipWindow : public UI::UniqueWindow {
 public:
-	ShipWindow(InteractiveGameBase& igb, UI::UniqueWindow::Registry& reg, Widelands::Ship& ship);
+	ShipWindow(InteractiveGameBase& igb, UI::UniqueWindow::Registry& reg, Widelands::Ship* ship);
 
 private:
 	void think() override;
@@ -47,6 +47,7 @@
 	                        const std::string& picname,
 	                        boost::function<void()> callback);
 	void set_button_visibility();
+	void no_port_error_message();
 
 	void act_goto();
 	void act_destination();
@@ -58,7 +59,7 @@
 	void act_explore_island(Widelands::IslandExploreDirection);
 
 	InteractiveGameBase& igbase_;
-	Widelands::Ship& ship_;
+	Widelands::OPtr<Widelands::Ship> ship_;
 
 	UI::Box vbox_;
 	UI::Box navigation_box_;
@@ -74,7 +75,7 @@
 	UI::Button* btn_construct_port_;
 	ItemWaresDisplay* display_;
 	int navigation_box_height_;
-	std::unique_ptr<Notifications::Subscriber<Widelands::NoteShipWindow>> shipnotes_subscriber_;
+	std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> shipnotes_subscriber_;
 	DISALLOW_COPY_AND_ASSIGN(ShipWindow);
 };
 

=== modified file 'src/wui/watchwindow.cc'
--- src/wui/watchwindow.cc	2018-04-07 16:59:00 +0000
+++ src/wui/watchwindow.cc	2018-04-16 08:14:47 +0000
@@ -31,66 +31,20 @@
 #include "logic/map_objects/bob.h"
 #include "logic/player.h"
 #include "profile/profile.h"
-#include "ui_basic/button.h"
-#include "ui_basic/window.h"
 #include "wui/interactive_gamebase.h"
 #include "wui/interactive_player.h"
-#include "wui/mapview.h"
 #include "wui/mapviewpixelconstants.h"
 #include "wui/mapviewpixelfunctions.h"
 
-#define NUM_VIEWS 5
 #define REFRESH_TIME 5000
 
 // Holds information for a view
-struct WatchWindowView {
-	MapView::View view;
-	Widelands::ObjectPointer tracking;  //  if non-null, we're tracking a Bob
-};
-
-struct WatchWindow : public UI::Window {
-	WatchWindow(InteractiveGameBase& parent,
-	            int32_t x,
-	            int32_t y,
-	            uint32_t w,
-	            uint32_t h,
-	            bool single_window_ = false);
-	~WatchWindow() override;
-
-	Widelands::Game& game() const {
-		return parent_.game();
-	}
-
-	boost::signals2::signal<void(Vector2f)> warp_mainview;
-
-	void add_view(Widelands::Coords);
-	void next_view();
-	void save_coords();
-	void close_cur_view();
-	void toggle_buttons();
-
-protected:
-	void think() override;
-	void stop_tracking_by_drag();
-	void draw(RenderTarget&) override;
-
-private:
-	void do_follow();
-	void do_goto();
-	void view_button_clicked(uint8_t index);
-	void set_current_view(uint8_t idx, bool save_previous = true);
-
-	InteractiveGameBase& parent_;
-	MapView map_view_;
-	uint32_t last_visit_;
-	bool single_window_;
-	uint8_t cur_index_;
-	UI::Button* view_btns_[NUM_VIEWS];
-	std::vector<WatchWindowView> views_;
-};
-
 static WatchWindow* g_watch_window = nullptr;
 
+Widelands::Game& WatchWindow::game() const {
+	return parent_.game();
+}
+
 WatchWindow::WatchWindow(InteractiveGameBase& parent,
                          int32_t const x,
                          int32_t const y,
@@ -98,8 +52,8 @@
                          uint32_t const h,
                          bool const init_single_window)
    : UI::Window(&parent, "watch", x, y, w, h, _("Watch")),
-     parent_(parent),
-     map_view_(this, game().map(), 0, 0, 200, 166),
+	  parent_(parent),
+	  map_view_(this, game().map(), 0, 0, 200, 166),
      last_visit_(game().get_gametime()),
      single_window_(init_single_window),
      cur_index_(0) {
@@ -115,7 +69,7 @@
 	gotobtn->sigclicked.connect(boost::bind(&WatchWindow::do_goto, this));
 
 	if (init_single_window) {
-		for (uint8_t i = 0; i < NUM_VIEWS; ++i) {
+		for (uint8_t i = 0; i < kViews; ++i) {
 			view_btns_[i] = new UI::Button(this, "view", 74 + (17 * i), 200 - 34, 17, 34,
 			                               g_gr->images().get("images/ui_basic/but0.png"), "-");
 			view_btns_[i]->sigclicked.connect(boost::bind(&WatchWindow::view_button_clicked, this, i));
@@ -128,24 +82,24 @@
 	}
 
 	map_view_.field_clicked.connect(
-	   [&parent](const Widelands::NodeAndTriangle<>& node_and_triangle) {
-		   parent.map_view()->field_clicked(node_and_triangle);
-		});
-	map_view_.track_selection.connect(
-	   [&parent](const Widelands::NodeAndTriangle<>& node_and_triangle) {
-		   parent.map_view()->track_selection(node_and_triangle);
-		});
-	map_view_.changeview.connect([this] { stop_tracking_by_drag(); });
+      [&parent](const Widelands::NodeAndTriangle<>& node_and_triangle) {
+              parent.map_view()->field_clicked(node_and_triangle);
+           });
+   map_view_.track_selection.connect(
+      [&parent](const Widelands::NodeAndTriangle<>& node_and_triangle) {
+              parent.map_view()->track_selection(node_and_triangle);
+           });
+   map_view_.changeview.connect([this] { stop_tracking_by_drag(); });
 	warp_mainview.connect([&parent](const Vector2f& map_pixel) {
 		parent.map_view()->scroll_to_map_pixel(map_pixel, MapView::Transition::Smooth);
 	});
 }
 
 void WatchWindow::draw(RenderTarget& dst) {
-	UI::Window::draw(dst);
-	if (!is_minimal()) {
-		parent_.draw_map_view(&map_view_, &dst);
-	}
+        UI::Window::draw(dst);
+        if (!is_minimal()) {
+                parent_.draw_map_view(&map_view_, &dst);
+        }
 }
 
 /**
@@ -154,9 +108,9 @@
  * This also resets the view cycling timer.
  */
 void WatchWindow::add_view(Widelands::Coords const coords) {
-	if (views_.size() >= NUM_VIEWS)
+	if (views_.size() >= kViews)
 		return;
-	WatchWindowView view;
+	WatchWindow::View view;
 
 	map_view_.scroll_to_field(coords, MapView::Transition::Jump);
 
@@ -183,7 +137,7 @@
 
 // Enables/Disables buttons for views_
 void WatchWindow::toggle_buttons() {
-	for (uint32_t i = 0; i < NUM_VIEWS; ++i) {
+	for (uint32_t i = 0; i < kViews; ++i) {
 		if (i < views_.size()) {
 			view_btns_[i]->set_title(std::to_string(i + 1));
 			view_btns_[i]->set_enabled(true);
@@ -228,7 +182,7 @@
 
 	if (upcast(Widelands::Bob, bob, views_[cur_index_].tracking.get(game()))) {
 		const Widelands::Map& map = game().map();
-		const Vector2f field_position = MapviewPixelFunctions::to_map_pixel(map, bob->get_position());
+      const Vector2f field_position = MapviewPixelFunctions::to_map_pixel(map, bob->get_position());
 		const Vector2f pos = bob->calc_drawpos(game(), field_position, 1.f);
 
 		// Drop the tracking if it leaves our vision range
@@ -256,6 +210,13 @@
 }
 
 /**
+ * Track the specified bob.
+ */
+void WatchWindow::follow(Widelands::Bob* bob) {
+	views_[cur_index_].tracking = bob;
+}
+
+/**
  * Called when the user clicks the "follow" button.
  *
  * If we are currently tracking a bob, stop tracking.
@@ -299,14 +260,14 @@
 				closest_dist = dist;
 			}
 		}
-		views_[cur_index_].tracking = closest;
+		follow(closest);
 	}
 }
 
 /**
  * Called when the "go to" button is clicked.
  *
- * Cause the main map_view_ to jump to our current position.
+ * Cause the main mapview_ to jump to our current position.
  */
 void WatchWindow::do_goto() {
 	warp_mainview(map_view_.view_area().rect().center());
@@ -344,14 +305,16 @@
 Open a watch window.
 ===============
 */
-void show_watch_window(InteractiveGameBase& parent, const Widelands::Coords& coords) {
+WatchWindow* show_watch_window(InteractiveGameBase& parent, const Widelands::Coords& coords) {
 	if (g_options.pull_section("global").get_bool("single_watchwin", false)) {
 		if (!g_watch_window) {
 			g_watch_window = new WatchWindow(parent, 250, 150, 200, 200, true);
 		}
 		g_watch_window->add_view(coords);
+		return g_watch_window;
 	} else {
 		auto* window = new WatchWindow(parent, 250, 150, 200, 200, false);
 		window->add_view(coords);
+		return window;
 	}
 }

=== modified file 'src/wui/watchwindow.h'
--- src/wui/watchwindow.h	2016-08-04 15:49:05 +0000
+++ src/wui/watchwindow.h	2018-04-16 08:14:47 +0000
@@ -22,8 +22,62 @@
 
 #include "logic/widelands_geometry.h"
 
+#include "ui_basic/button.h"
+#include "ui_basic/window.h"
+#include "wui/mapview.h"
+
 class InteractiveGameBase;
-
-void show_watch_window(InteractiveGameBase&, const Widelands::Coords&);
+namespace Widelands {
+class Game;
+}
+
+struct WatchWindow : public UI::Window {
+	WatchWindow(InteractiveGameBase& parent,
+					int32_t x,
+					int32_t y,
+					uint32_t w,
+					uint32_t h,
+					bool single_window_ = false);
+	~WatchWindow();
+
+	boost::signals2::signal<void(Vector2f)> warp_mainview;
+
+	void add_view(Widelands::Coords);
+	void follow(Widelands::Bob* bob);
+
+private:
+	static constexpr size_t kViews = 5;
+
+	// Holds information for a view
+	struct View {
+		MapView::View view;
+		Widelands::ObjectPointer tracking;  //  if non-null, we're tracking a Bob
+	};
+
+	Widelands::Game& game() const;
+
+	void think() override;
+	void stop_tracking_by_drag();
+	void draw(RenderTarget&) override;
+	void save_coords();
+	void next_view();
+	void close_cur_view();
+	void toggle_buttons();
+
+	void do_follow();
+	void do_goto();
+	void view_button_clicked(uint8_t index);
+	void set_current_view(uint8_t idx, bool save_previous = true);
+
+	InteractiveGameBase& parent_;
+	MapView map_view_;
+	uint32_t last_visit_;
+	bool single_window_;
+	uint8_t cur_index_;
+	UI::Button* view_btns_[kViews];
+	std::vector<WatchWindow::View> views_;
+};
+
+WatchWindow* show_watch_window(InteractiveGameBase&, const Widelands::Coords&);
 
 #endif  // end of include guard: WL_WUI_WATCHWINDOW_H


Follow ups