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

Proposed by GunChleoc
Status: Merged
Merged at revision: 8669
Proposed branch: lp:~widelands-dev/widelands/bug-1358880-ship-statistics
Merge into: lp:widelands
Prerequisite: lp:~widelands-dev/widelands/ships_optr
Diff against target: 2040 lines (+1087/-215)
23 files modified
data/tribes/scripting/help/controls.lua (+39/-12)
src/ai/defaultai.cc (+10/-6)
src/ai/defaultai.h (+1/-1)
src/game_io/game_player_info_packet.cc (+2/-2)
src/logic/map_objects/tribes/ship.cc (+39/-32)
src/logic/map_objects/tribes/ship.h (+9/-20)
src/logic/player.cc (+27/-3)
src/logic/player.h (+8/-1)
src/notifications/note_ids.h (+1/-2)
src/scripting/lua_game.cc (+7/-21)
src/ui_basic/table.cc (+12/-0)
src/ui_basic/table.h (+25/-3)
src/wui/CMakeLists.txt (+2/-0)
src/wui/game_statistics_menu.cc (+12/-1)
src/wui/interactive_gamebase.cc (+21/-18)
src/wui/interactive_gamebase.h (+2/-0)
src/wui/interactive_player.cc (+11/-0)
src/wui/seafaring_statistics_menu.cc (+580/-0)
src/wui/seafaring_statistics_menu.h (+159/-0)
src/wui/shipwindow.cc (+42/-33)
src/wui/shipwindow.h (+2/-1)
src/wui/watchwindow.cc (+20/-57)
src/wui/watchwindow.h (+56/-2)
To merge this branch: bzr merge lp:~widelands-dev/widelands/bug-1358880-ship-statistics
Reviewer Review Type Date Requested Status
Notabilis Approve
GunChleoc Needs Resubmitting
Review via email: mp+343293@code.launchpad.net

Commit message

Adds a new Ship Statistics window.

Description of the change

Finally, ths ship list.

lp:~widelands-dev/widelands/ships_optr needs to go in first.

To post a comment you must log in.
Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 3386. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/367231513.
Appveyor build 3192. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_bug_1358880_ship_statistics-3192.

Revision history for this message
Notabilis (notabilis27) wrote :

Great work, I really like the new list!

Most of the code looks good, three small bugs and some other comments, though.

The bugs:
- Hotkey 'e' even works on non-seafaring maps
- Missing "ship name" column in the list when there currently are no ships
- Memory leak with create_shipinfo

Two non code related comments (feel free to ignore) :
- I would like a button for opening the ship window without actually moving there. This could, e.g., be useful to start constructing a port while busy elsewhere. Currently both buttons move the view to the ship, maybe have one button for the window and one button to move to the ship?
- I am not really happy with calling the window "ship statistics" (in code and UI). For me, that sounds like a graph showing the number of ships (or so), and not like a list of all ships.

For the other comments, see the diff.

review: Needs Fixing
Revision history for this message
GunChleoc (gunchleoc) wrote :

Thanks for the review! Everything that I haven't added a comment to, I will implement exactly as you suggested.

As to the name - I guess that's for consistency, just like we have the "Building Statistics", which is also a mix of info and unit access. I am open to suggestions though :)

Revision history for this message
Notabilis (notabilis27) wrote :

I guess I would have called it "Ship List" or something like that. But consistency is a good argument, lets leave it as it is.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have now fixed everything except maybe for this one:

- Memory leak with create_shipinfo

I thought I had caught that one already, but I have worked on the function just in case - it now returns a unique_ptr. That function had also produced a compiler warning which I fixed (yay for upgrading my Ubuntu). Do you have steps to reproduce the memory leak?

review: Needs Resubmitting
Revision history for this message
Notabilis (notabilis27) wrote :

Thanks for the fixes/changes! Testing and reviewing the commits went fine.
One really minor nit: Some indentations are now not correctly aligned (some assert() somewhere). ;)

Unfortunately I don't know any steps to reproduce the memory leak. It didn't seem to turn up every time when I tested it. But the changes look good and I played around with the window and wasn't able to trigger any further ASAN complains, so I guess its fine now.

review: Approve
Revision history for this message
GunChleoc (gunchleoc) wrote :

Thanks for the review and testing again!

I am still tweaking my IDE's layout function as best as I can. bunnybot will take care of the misaligned assert :)

@bunnybot merge

Revision history for this message
bunnybot (widelandsofficial) wrote :

Refusing to merge, since Travis is not green. Use @bunnybot merge force for merging anyways.

Travis build 3386. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/367231513.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Transient failure on Travis

@bunnybot merge force

Revision history for this message
kaputtnik (franku) wrote :

Finally we got this!!!

Thanks GunChleoc :-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'data/images/wui/editor/fsel_editor_set_port_space.png'
2Binary 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-28 09:26:35 +0000 differ
3=== removed file 'data/images/wui/editor/fsel_editor_set_port_space.png'
4Binary 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
5=== added file 'data/images/wui/editor/fsel_editor_unset_port_space.png'
6Binary 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-28 09:26:35 +0000 differ
7=== removed file 'data/images/wui/editor/fsel_editor_unset_port_space.png'
8Binary 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
9=== added file 'data/images/wui/ship/ship_construct_port_space.png'
10Binary 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-28 09:26:35 +0000 differ
11=== added file 'data/images/wui/stats/ship_stats_idle.png'
12Binary 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-28 09:26:35 +0000 differ
13=== added file 'data/images/wui/stats/ship_stats_shipping.png'
14Binary 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-28 09:26:35 +0000 differ
15=== modified file 'data/tribes/scripting/help/controls.lua'
16--- data/tribes/scripting/help/controls.lua 2017-12-06 08:16:46 +0000
17+++ data/tribes/scripting/help/controls.lua 2018-04-28 09:26:35 +0000
18@@ -21,6 +21,16 @@
19 -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
20 dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Left-click on Button")), _"Skip confirmation dialog")) ..
21
22+ h2(_"Table Control") ..
23+ h3(_"In tables that allow the selection of multiple entries, the following key combinations are available:") ..
24+ p(
25+ -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
26+ dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Click")), pgettext("table_control", "Select multiple entries")) ..
27+ -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
28+ dl(help_format_hotkey(pgettext("hotkey", "Shift + Click")), pgettext("table_control", "Select a range of entries")) ..
29+ -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
30+ dl(help_format_hotkey(pgettext("hotkey", "Ctrl + A")), pgettext("table_control", "Select all entries"))) ..
31+
32 h2(_"Road Control") ..
33 p(
34 -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
35@@ -52,6 +62,8 @@
36 dl(help_format_hotkey("I"), _"Toggle stock inventory") ..
37 -- TRANSLATORS: This is an access key combination. The hotkey is 'b'
38 dl(help_format_hotkey("B"), _"Toggle building statistics") ..
39+ -- TRANSLATORS: This is an access key combination. The hotkey is 'e'
40+ dl(help_format_hotkey("E"), _"Toggle seafaring statistics") ..
41 -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
42 dl(help_format_hotkey(pgettext("hotkey", "Home")), _"Center main mapview on starting location") ..
43 -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
44@@ -72,18 +84,8 @@
45 dl(help_format_hotkey(pgettext("hotkey", "F6")), _"Show the debug console (only in debug-builds)")
46 ) ..
47
48- h2(_"Table Control") ..
49- h3(_"In tables that allow the selection of multiple entries, the following key combinations are available:") ..
50- p(
51- -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
52- dl(help_format_hotkey(pgettext("hotkey", "Ctrl + Click")), pgettext("table_control", "Select multiple entries")) ..
53- -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
54- dl(help_format_hotkey(pgettext("hotkey", "Shift + Click")), pgettext("table_control", "Select a range of entries")) ..
55- -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
56- dl(help_format_hotkey(pgettext("hotkey", "Ctrl + A")), pgettext("table_control", "Select all entries"))) ..
57-
58+ -- TRANSLATORS: Heading in "Controls" help
59 h2(_"Message Window") ..
60- h3(_"In the message window, the following additional shortcuts are available:") ..
61 p(
62 -- TRANSLATORS: This is the helptext for an access key combination.
63 dl(help_format_hotkey(pgettext("hotkey", "Alt + 0")), _"Show all messages") ..
64@@ -101,5 +103,30 @@
65 dl(help_format_hotkey("G"), _"Jump to the location corresponding to the current message") ..
66 -- TRANSLATORS: This is an access key combination. Localize, but do not change the key.
67 dl(help_format_hotkey(pgettext("hotkey", "Delete")), _"Archive/Restore the current message")
68- )
69+ ) ..
70+
71+ -- TRANSLATORS: Heading in "Controls" help
72+ h2(_"Ship Statistics") ..
73+ p(
74+ -- TRANSLATORS: This is the helptext for an access key combination.
75+ dl(help_format_hotkey(pgettext("hotkey", "Alt + 0")), _"Show all ships") ..
76+ -- TRANSLATORS: This is the helptext for an access key combination.
77+ dl(help_format_hotkey(pgettext("hotkey", "Alt + 1")), _"Show idle ships") ..
78+ -- TRANSLATORS: This is the helptext for an access key combination.
79+ dl(help_format_hotkey(pgettext("hotkey", "Alt + 2")), _"Show ships shipping wares and workers") ..
80+ -- TRANSLATORS: This is the helptext for an access key combination.
81+ dl(help_format_hotkey(pgettext("hotkey", "Alt + 3")), _"Show waiting expeditions") ..
82+ -- TRANSLATORS: This is the helptext for an access key combination.
83+ dl(help_format_hotkey(pgettext("hotkey", "Alt + 4")), _"Show scouting expeditions") ..
84+ -- TRANSLATORS: This is the helptext for an access key combination.
85+ dl(help_format_hotkey(pgettext("hotkey", "Alt + 5")), _"Show expeditions that have found a port space or are founding a colony") ..
86+ -- TRANSLATORS: This is the helptext for an access key combination.
87+ dl(help_format_hotkey("G"), _"Center the map on the selected ship") ..
88+ -- TRANSLATORS: This is the helptext for an access key combination.
89+ dl(help_format_hotkey("O"), _"Open the selected ship’s window") ..
90+ -- TRANSLATORS: This is the helptext for an access key combination.
91+ dl(help_format_hotkey("CTRL + O"), _"Go to the selected ship and open its window") ..
92+ -- TRANSLATORS: This is the helptext for an access key combination.
93+ dl(help_format_hotkey("W"), _"Watch the selected ship")
94+ )
95 }
96
97=== modified file 'src/ai/defaultai.cc'
98--- src/ai/defaultai.cc 2018-04-08 22:33:43 +0000
99+++ src/ai/defaultai.cc 2018-04-28 09:26:35 +0000
100@@ -167,8 +167,8 @@
101 });
102
103 // Subscribe to ShipNotes.
104- shipnotes_subscriber_ = Notifications::subscribe<NoteShipMessage>([this](
105- const NoteShipMessage& note) {
106+ shipnotes_subscriber_ = Notifications::subscribe<NoteShip>([this](
107+ const NoteShip& note) {
108
109 // in a short time between start and late_initialization the player
110 // can get notes that can not be processed.
111@@ -180,13 +180,13 @@
112 return;
113 }
114
115- switch (note.message) {
116+ switch (note.action) {
117
118- case NoteShipMessage::Message::kGained:
119+ case NoteShip::Action::kGained:
120 gain_ship(*note.ship, NewShip::kBuilt);
121 break;
122
123- case NoteShipMessage::Message::kLost:
124+ case NoteShip::Action::kLost:
125 for (std::deque<ShipObserver>::iterator i = allships.begin(); i != allships.end(); ++i) {
126 if (i->ship == note.ship) {
127 allships.erase(i);
128@@ -195,15 +195,19 @@
129 }
130 break;
131
132- case NoteShipMessage::Message::kWaitingForCommand:
133+ case NoteShip::Action::kWaitingForCommand:
134 for (std::deque<ShipObserver>::iterator i = allships.begin(); i != allships.end(); ++i) {
135 if (i->ship == note.ship) {
136 i->waiting_for_command_ = true;
137 break;
138 }
139 }
140+ default:
141+ // Do nothing
142+ break;
143 }
144 });
145+
146 }
147
148 DefaultAI::~DefaultAI() {
149
150=== modified file 'src/ai/defaultai.h'
151--- src/ai/defaultai.h 2018-04-08 22:33:43 +0000
152+++ src/ai/defaultai.h 2018-04-28 09:26:35 +0000
153@@ -414,7 +414,7 @@
154 outofresource_subscriber_;
155 std::unique_ptr<Notifications::Subscriber<Widelands::NoteTrainingSiteSoldierTrained>>
156 soldiertrained_subscriber_;
157- std::unique_ptr<Notifications::Subscriber<Widelands::NoteShipMessage>> shipnotes_subscriber_;
158+ std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> shipnotes_subscriber_;
159 };
160
161 #endif // end of include guard: WL_AI_DEFAULTAI_H
162
163=== modified file 'src/game_io/game_player_info_packet.cc'
164--- src/game_io/game_player_info_packet.cc 2018-04-07 16:59:00 +0000
165+++ src/game_io/game_player_info_packet.cc 2018-04-28 09:26:35 +0000
166@@ -30,7 +30,7 @@
167
168 namespace Widelands {
169
170-constexpr uint16_t kCurrentPacketVersion = 20;
171+constexpr uint16_t kCurrentPacketVersion = 21;
172
173 void GamePlayerInfoPacket::read(FileSystem& fs, Game& game, MapObjectLoader*) {
174 try {
175@@ -60,7 +60,7 @@
176
177 player->set_ai(fr.c_string());
178 player->read_statistics(fr);
179- player->read_remaining_shipnames(fr);
180+ player->read_remaining_shipnames(fr, packet_version);
181
182 player->casualties_ = fr.unsigned_32();
183 player->kills_ = fr.unsigned_32();
184
185=== modified file 'src/logic/map_objects/tribes/ship.cc'
186--- src/logic/map_objects/tribes/ship.cc 2018-04-07 16:59:00 +0000
187+++ src/logic/map_objects/tribes/ship.cc 2018-04-28 09:26:35 +0000
188@@ -133,7 +133,6 @@
189 }
190
191 Ship::~Ship() {
192- Notifications::publish(NoteShipWindow(serial(), NoteShipWindow::Action::kClose));
193 }
194
195 PortDock* Ship::get_destination(EditorGameBase& egbase) const {
196@@ -155,12 +154,13 @@
197 bool Ship::init(EditorGameBase& egbase) {
198 Bob::init(egbase);
199 init_fleet(egbase);
200- Notifications::publish(NoteShipMessage(this, NoteShipMessage::Message::kGained));
201 assert(get_owner());
202+ get_owner()->add_ship(serial());
203
204 // Assigning a ship name
205 shipname_ = get_owner()->pick_shipname();
206 molog("New ship: %s\n", shipname_.c_str());
207+ Notifications::publish(NoteShip(this, NoteShip::Action::kGained));
208 return true;
209 }
210
211@@ -182,12 +182,17 @@
212 fleet_->remove_ship(egbase, this);
213 }
214
215+ Player* o = get_owner();
216+ if (o != nullptr) {
217+ o->remove_ship(serial());
218+ }
219+
220 while (!items_.empty()) {
221 items_.back().remove(egbase);
222 items_.pop_back();
223 }
224
225- Notifications::publish(NoteShipMessage(this, NoteShipMessage::Message::kLost));
226+ Notifications::publish(NoteShip(this, NoteShip::Action::kLost));
227
228 Bob::cleanup(egbase);
229 }
230@@ -279,7 +284,7 @@
231 case ShipStates::kSinkAnimation:
232 // The sink animation has been played, so finally remove the ship from the map
233 pop_task(game);
234- remove(game);
235+ schedule_destroy(game);
236 return;
237 }
238 // if the real update function failed (e.g. nothing to transport), the ship goes idle
239@@ -304,6 +309,7 @@
240 destination_ = nullptr;
241 dst->ship_arrived(game, *this);
242 start_task_idle(game, descr().main_animation(), 250);
243+ Notifications::publish(NoteShip(this, NoteShip::Action::kDestinationChanged));
244 return true;
245 }
246
247@@ -411,17 +417,13 @@
248 }
249 } while (mr.advance(*map));
250
251+ expedition_->seen_port_buildspaces = temp_port_buildspaces;
252 if (new_port_space) {
253- ship_state_ = ShipStates::kExpeditionPortspaceFound;
254+ set_ship_state_and_notify(ShipStates::kExpeditionPortspaceFound, NoteShip::Action::kWaitingForCommand);
255 send_message(game, _("Port Space"), _("Port Space Found"),
256 _("An expedition ship found a new port build space."),
257 "images/wui/editor/fsel_editor_set_port_space.png");
258 }
259- expedition_->seen_port_buildspaces = temp_port_buildspaces;
260- if (new_port_space) {
261- Notifications::publish(
262- NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
263- }
264 }
265 }
266
267@@ -532,17 +534,13 @@
268 } else {
269 // Check whether the island was completely surrounded
270 if (get_position() == expedition_->exploration_start) {
271+ set_ship_state_and_notify(ShipStates::kExpeditionWaiting, NoteShip::Action::kWaitingForCommand);
272 send_message(game,
273 /** TRANSLATORS: A ship has circumnavigated an island and is waiting
274 for orders */
275 pgettext("ship", "Waiting"), _("Island Circumnavigated"),
276 _("An expedition ship sailed around its island without any events."),
277 "images/wui/ship/ship_explore_island_cw.png");
278- ship_state_ = ShipStates::kExpeditionWaiting;
279-
280- Notifications::publish(
281- NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
282-
283 return start_task_idle(game, descr().main_animation(), 1500);
284 }
285 }
286@@ -582,10 +580,10 @@
287 }
288 }
289 // if we are here, it seems something really strange happend.
290- log("WARNING: ship %s was not able to start exploration. Entering WAIT mode.\n",
291- shipname_.c_str());
292- ship_state_ = ShipStates::kExpeditionWaiting;
293- return start_task_idle(game, descr().main_animation(), 1500);
294+ log("WARNING: ship %s was not able to start exploration. Entering WAIT mode.", shipname_.c_str());
295+ set_ship_state_and_notify(ShipStates::kExpeditionWaiting, NoteShip::Action::kWaitingForCommand);
296+ start_task_idle(game, descr().main_animation(), 1500);
297+ return;
298 }
299 } else { // scouting towards a specific direction
300 if (exp_dir_swimmable(expedition_->scouting_direction)) {
301@@ -595,7 +593,7 @@
302 return;
303 }
304 // coast reached
305- ship_state_ = ShipStates::kExpeditionWaiting;
306+ set_ship_state_and_notify(ShipStates::kExpeditionWaiting, NoteShip::Action::kWaitingForCommand);
307 start_task_idle(game, descr().main_animation(), 1500);
308 // Send a message to the player, that a new coast was reached
309 send_message(game,
310@@ -603,10 +601,6 @@
311 _("Land Ahoy!"), _("Coast Reached"),
312 _("An expedition ship reached a coast and is waiting for further commands."),
313 "images/wui/ship/ship_scout_ne.png");
314-
315- Notifications::publish(
316- NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
317-
318 return;
319 }
320 }
321@@ -710,6 +704,13 @@
322 NEVER_HERE();
323 }
324
325+void Ship::set_ship_state_and_notify(ShipStates state, NoteShip::Action action) {
326+ if (ship_state_ != state) {
327+ ship_state_ = state;
328+ Notifications::publish(NoteShip(this, action));
329+ }
330+}
331+
332 void Ship::set_economy(Game& game, Economy* e) {
333 // Do not check here that the economy actually changed, because on loading
334 // we rely that wares really get reassigned our economy.
335@@ -730,6 +731,7 @@
336 items_.size());
337 destination_ = &pd;
338 send_signal(game, "wakeup");
339+ Notifications::publish(NoteShip(this, NoteShip::Action::kDestinationChanged));
340 }
341
342 void Ship::add_item(Game& game, const ShippingItem& item) {
343@@ -847,14 +849,14 @@
344 pgettext("ship", "Expedition"), _("Expedition Ready"),
345 _("An expedition ship is waiting for your commands."),
346 "images/wui/buildings/start_expedition.png");
347- Notifications::publish(NoteShipMessage(this, NoteShipMessage::Message::kWaitingForCommand));
348+ Notifications::publish(NoteShip(this, NoteShip::Action::kWaitingForCommand));
349 }
350
351 /// Initializes / changes the direction of scouting to @arg direction
352 /// @note only called via player command
353 void Ship::exp_scouting_direction(Game&, WalkingDir scouting_direction) {
354 assert(expedition_);
355- ship_state_ = ShipStates::kExpeditionScouting;
356+ set_ship_state_and_notify(ShipStates::kExpeditionScouting, NoteShip::Action::kDestinationChanged);
357 expedition_->scouting_direction = scouting_direction;
358 expedition_->island_exploration = false;
359 }
360@@ -880,7 +882,7 @@
361 for (auto& tree : trees) {
362 tree.object->remove(game);
363 }
364- ship_state_ = ShipStates::kExpeditionColonizing;
365+ set_ship_state_and_notify(ShipStates::kExpeditionColonizing, NoteShip::Action::kDestinationChanged);
366 }
367
368 /// Initializes / changes the direction the island exploration in @arg island_explore_direction
369@@ -888,7 +890,7 @@
370 /// @note only called via player command
371 void Ship::exp_explore_island(Game&, IslandExploreDirection island_explore_direction) {
372 assert(expedition_);
373- ship_state_ = ShipStates::kExpeditionScouting;
374+ set_ship_state_and_notify(ShipStates::kExpeditionScouting, NoteShip::Action::kDestinationChanged);
375 expedition_->island_explore_direction = island_explore_direction;
376 expedition_->scouting_direction = WalkingDir::IDLE;
377 expedition_->island_exploration = true;
378@@ -945,7 +947,7 @@
379 }
380 }
381
382- Notifications::publish(NoteShipWindow(serial(), NoteShipWindow::Action::kNoPortLeft));
383+ Notifications::publish(NoteShip(this, NoteShip::Action::kNoPortLeft));
384 return;
385 }
386 assert(get_economy() && get_economy() != expedition_->economy.get());
387@@ -962,7 +964,6 @@
388 // Running colonization has the highest priority + a sink request is only valid once
389 if (!state_is_sinkable())
390 return;
391- Notifications::publish(NoteShipWindow(serial(), NoteShipWindow::Action::kClose));
392 ship_state_ = ShipStates::kSinkRequest;
393 // Make sure the ship is active and close possible open windows
394 ship_wakeup(game);
395@@ -980,8 +981,13 @@
396 if (draw_text & TextToDraw::kStatistics) {
397 switch (ship_state_) {
398 case (ShipStates::kTransport):
399- /** TRANSLATORS: This is a ship state */
400- statistics_string = pgettext("ship_state", "Shipping");
401+ if (destination_.is_set()) {
402+ /** TRANSLATORS: This is a ship state. The ship is currently transporting wares. */
403+ statistics_string = pgettext("ship_state", "Shipping");
404+ } else {
405+ /** TRANSLATORS: This is a ship state. The ship is ready to transport wares, but has nothing to do. */
406+ statistics_string = pgettext("ship_state", "Idle");
407+ }
408 break;
409 case (ShipStates::kExpeditionWaiting):
410 /** TRANSLATORS: This is a ship state. An expedition is waiting for your commands. */
411@@ -1182,6 +1188,7 @@
412 // economy. Also, we might are on an expedition which means that we just now
413 // created the economy of this ship and must inform all wares.
414 ship.set_economy(dynamic_cast<Game&>(egbase()), ship.economy_);
415+ ship.get_owner()->add_ship(ship.serial());
416 }
417
418 MapObject::Loader* Ship::load(EditorGameBase& egbase, MapObjectLoader& mol, FileRead& fr) {
419
420=== modified file 'src/logic/map_objects/tribes/ship.h'
421--- src/logic/map_objects/tribes/ship.h 2018-04-07 16:59:00 +0000
422+++ src/logic/map_objects/tribes/ship.h 2018-04-28 09:26:35 +0000
423@@ -41,29 +41,16 @@
424 kNotSet
425 };
426
427-struct NoteShipMessage {
428- CAN_BE_SENT_AS_NOTE(NoteId::ShipMessage)
429+struct NoteShip {
430+ CAN_BE_SENT_AS_NOTE(NoteId::Ship)
431
432 Ship* ship;
433
434- enum class Message { kLost, kGained, kWaitingForCommand };
435- Message message;
436-
437- NoteShipMessage(Ship* const init_ship, const Message& init_message)
438- : ship(init_ship), message(init_message) {
439- }
440-};
441-
442-struct NoteShipWindow {
443- CAN_BE_SENT_AS_NOTE(NoteId::ShipWindow)
444-
445- Serial serial;
446-
447- enum class Action { kClose, kNoPortLeft };
448- const Action action;
449-
450- NoteShipWindow(Serial init_serial, const Action& init_action)
451- : serial(init_serial), action(init_action) {
452+ enum class Action { kDestinationChanged, kWaitingForCommand, kNoPortLeft, kLost, kGained };
453+ Action action;
454+
455+ NoteShip(Ship* const init_ship, const Action& init_action)
456+ : ship(init_ship), action(init_action) {
457 }
458 };
459
460@@ -263,6 +250,8 @@
461 bool ship_update_transport(Game&, State&);
462 void ship_update_expedition(Game&, State&);
463 void ship_update_idle(Game&, State&);
464+ /// Set the ship's state to 'state' and if the ship state has changed, publish a notification.
465+ void set_ship_state_and_notify(ShipStates state, NoteShip::Action action);
466
467 bool init_fleet(EditorGameBase&);
468 void set_fleet(Fleet* fleet);
469
470=== modified file 'src/logic/player.cc'
471--- src/logic/player.cc 2018-04-07 16:59:00 +0000
472+++ src/logic/player.cc 2018-04-28 09:26:35 +0000
473@@ -23,6 +23,7 @@
474 #include <memory>
475
476 #include <boost/bind.hpp>
477+#include <boost/format.hpp>
478 #include <boost/signals2.hpp>
479
480 #include "base/i18n.h"
481@@ -133,6 +134,7 @@
482 msites_defeated_(0),
483 civil_blds_lost_(0),
484 civil_blds_defeated_(0),
485+ ship_name_counter_(0),
486 fields_(nullptr),
487 allowed_worker_types_(the_egbase.tribes().nrworkers(), true),
488 allowed_building_types_(the_egbase.tribes().nrbuildings(), true),
489@@ -372,6 +374,19 @@
490 game->cmdqueue().enqueue(new CmdDeleteMessage(game->get_gametime(), player_number_, message_id));
491 }
492
493+const std::set<Serial>& Player::ships() const {
494+ return ships_;
495+}
496+void Player::add_ship(Serial ship) {
497+ ships_.insert(ship);
498+}
499+void Player::remove_ship(Serial ship) {
500+ auto it = ships_.find(ship);
501+ if (it != ships_.end()) {
502+ ships_.erase(it);
503+ }
504+}
505+
506 /*
507 ===============
508 Return filtered buildcaps that take the player's territory into account.
509@@ -1293,6 +1308,8 @@
510 * Pick random name from remaining names (if any)
511 */
512 const std::string Player::pick_shipname() {
513+ ++ship_name_counter_;
514+
515 if (!remaining_shipnames_.empty()) {
516 Game& game = dynamic_cast<Game&>(egbase());
517 assert(is_a(Game, &egbase()));
518@@ -1303,7 +1320,7 @@
519 remaining_shipnames_.erase(it);
520 return new_name;
521 }
522- return "Ship";
523+ return (boost::format(pgettext("shipname", "Ship %d")) % ship_name_counter_).str();
524 }
525
526 /**
527@@ -1311,13 +1328,19 @@
528 *
529 * \param fr source stream
530 */
531-void Player::read_remaining_shipnames(FileRead& fr) {
532+void Player::read_remaining_shipnames(FileRead& fr, uint16_t packet_version) {
533 // First get rid of default shipnames
534 remaining_shipnames_.clear();
535 const uint16_t count = fr.unsigned_16();
536 for (uint16_t i = 0; i < count; ++i) {
537 remaining_shipnames_.insert(fr.string());
538 }
539+ // TODO(GunChleoc): Savegame compatibility. Remove after Build 20.
540+ if (packet_version >= 21) {
541+ ship_name_counter_ = fr.unsigned_32();
542+ } else {
543+ ship_name_counter_ = ships_.size();
544+ }
545 }
546
547 /**
548@@ -1396,13 +1419,14 @@
549 }
550
551 /**
552- * Write remaining ship indexes to the give file
553+ * Write remaining ship indexes to the given file
554 */
555 void Player::write_remaining_shipnames(FileWrite& fw) const {
556 fw.unsigned_16(remaining_shipnames_.size());
557 for (const auto& shipname : remaining_shipnames_) {
558 fw.string(shipname);
559 }
560+ fw.unsigned_32(ship_name_counter_);
561 }
562
563 /**
564
565=== modified file 'src/logic/player.h'
566--- src/logic/player.h 2018-04-07 16:59:00 +0000
567+++ src/logic/player.h 2018-04-28 09:26:35 +0000
568@@ -111,6 +111,10 @@
569 get_messages()->set_message_status(id, status);
570 }
571
572+ const std::set<Serial>& ships() const;
573+ void add_ship(Serial ship);
574+ void remove_ship(Serial ship);
575+
576 const EditorGameBase& egbase() const {
577 return egbase_;
578 }
579@@ -583,7 +587,7 @@
580
581 void read_statistics(FileRead&);
582 void write_statistics(FileWrite&) const;
583- void read_remaining_shipnames(FileRead&);
584+ void read_remaining_shipnames(FileRead&, uint16_t packet_version);
585 void write_remaining_shipnames(FileWrite&) const;
586 void sample_statistics();
587 void ware_produced(DescriptionIndex);
588@@ -634,11 +638,14 @@
589 uint32_t msites_lost_, msites_defeated_;
590 uint32_t civil_blds_lost_, civil_blds_defeated_;
591 std::unordered_set<std::string> remaining_shipnames_;
592+ // If we run out of ship names, we'll want to continue with unique numbers
593+ uint32_t ship_name_counter_;
594
595 Field* fields_;
596 std::vector<bool> allowed_worker_types_;
597 std::vector<bool> allowed_building_types_;
598 Economies economies_;
599+ std::set<Serial> ships_;
600 std::string name_; // Player name
601 std::string ai_; /**< Name of preferred AI implementation */
602
603
604=== modified file 'src/notifications/note_ids.h'
605--- src/notifications/note_ids.h 2018-04-07 16:59:00 +0000
606+++ src/notifications/note_ids.h 2018-04-28 09:26:35 +0000
607@@ -33,8 +33,7 @@
608 FieldTerrainChanged,
609 ProductionSiteOutOfResources,
610 TrainingSiteSoldierTrained,
611- ShipMessage,
612- ShipWindow,
613+ Ship,
614 Building,
615 Economy,
616 GraphicResolutionChanged,
617
618=== modified file 'src/scripting/lua_game.cc'
619--- src/scripting/lua_game.cc 2018-04-07 16:59:00 +0000
620+++ src/scripting/lua_game.cc 2018-04-28 09:26:35 +0000
621@@ -672,30 +672,16 @@
622 */
623 int LuaPlayer::get_ships(lua_State* L) {
624 EditorGameBase& egbase = get_egbase(L);
625- const Map& map = egbase.map();
626 PlayerNumber p = (get(L, egbase)).player_number();
627 lua_newtable(L);
628 uint32_t cidx = 1;
629-
630- std::set<OPtr<Ship>> found_ships;
631- for (int16_t y = 0; y < map.get_height(); ++y) {
632- for (int16_t x = 0; x < map.get_width(); ++x) {
633- FCoords f = map.get_fcoords(Coords(x, y));
634- // there are too many bobs on the map so we investigate
635- // only bobs on water
636- if (f.field->nodecaps() & MOVECAPS_SWIM) {
637- for (Bob* bob = f.field->get_first_bob(); bob; bob = bob->get_next_on_field()) {
638- if (upcast(Ship, ship, bob)) {
639- if (ship->get_owner()->player_number() == p && !found_ships.count(ship)) {
640- found_ships.insert(ship);
641- lua_pushuint32(L, cidx++);
642- LuaMaps::upcasted_map_object_to_lua(L, ship);
643- lua_rawset(L, -3);
644- }
645- }
646- }
647- }
648- }
649+ for (const auto& serial : egbase.player(p).ships()) {
650+ Widelands::MapObject* obj = egbase.objects().get_object(serial);
651+ assert(obj->descr().type() == Widelands::MapObjectType::SHIP);
652+ upcast(Widelands::Ship, ship, obj);
653+ lua_pushuint32(L, cidx++);
654+ LuaMaps::upcasted_map_object_to_lua(L, ship);
655+ lua_rawset(L, -3);
656 }
657 return 1;
658 }
659
660=== modified file 'src/ui_basic/table.cc'
661--- src/ui_basic/table.cc 2018-04-07 16:59:00 +0000
662+++ src/ui_basic/table.cc 2018-04-28 09:26:35 +0000
663@@ -551,6 +551,18 @@
664 layout();
665 }
666
667+/**
668+ * Remove the given table entry if it exists.
669+ */
670+void Table<void*>::remove_entry(const void* const entry) {
671+ for (uint32_t i = 0; i < entry_records_.size(); ++i) {
672+ if (entry_records_[i]->entry() == entry) {
673+ remove(i);
674+ return;
675+ }
676+ }
677+}
678+
679 bool Table<void*>::sort_helper(uint32_t a, uint32_t b) {
680 if (sort_descending_) {
681 return columns_[sort_column_].compare(b, a);
682
683=== modified file 'src/ui_basic/table.h'
684--- src/ui_basic/table.h 2018-04-07 16:59:00 +0000
685+++ src/ui_basic/table.h 2018-04-28 09:26:35 +0000
686@@ -84,6 +84,7 @@
687
688 void sort(uint32_t lower_bound = 0, uint32_t upper_bound = std::numeric_limits<uint32_t>::max());
689 void remove(uint32_t);
690+ void remove_entry(Entry);
691
692 EntryRecord& add(void* const entry, const bool select_this = false);
693
694@@ -150,7 +151,7 @@
695 return clr;
696 }
697
698- private:
699+ private:
700 friend class Table<void*>;
701 void* entry_;
702 bool use_clr;
703@@ -190,8 +191,6 @@
704 void set_column_title(uint8_t col, const std::string& title);
705 void set_column_compare(uint8_t col, const CompareFn& fn);
706
707- void layout() override;
708-
709 void clear();
710 void set_sort_column(uint8_t const col) {
711 assert(col < columns_.size());
712@@ -209,6 +208,7 @@
713
714 void sort(uint32_t lower_bound = 0, uint32_t upper_bound = std::numeric_limits<uint32_t>::max());
715 void remove(uint32_t);
716+ void remove_entry(const void* const entry);
717
718 EntryRecord& add(void* entry = nullptr, bool select = false);
719
720@@ -275,6 +275,8 @@
721 /// If entries == 0, the current entries are used.
722 void fit_height(uint32_t entries = 0);
723
724+ void layout() override;
725+
726 // Drawing and event handling
727 void draw(RenderTarget&) override;
728 bool handle_mousepress(uint8_t btn, int32_t x, int32_t y) override;
729@@ -335,6 +337,10 @@
730 : Base(parent, x, y, w, h, button_background, rowtype) {
731 }
732
733+ void remove_entry(Entry const* const entry) {
734+ Base::remove_entry(const_cast<Entry*>(entry));
735+ }
736+
737 EntryRecord& add(Entry const* const entry = 0, bool const select_this = false) {
738 return Base::add(const_cast<Entry*>(entry), select_this);
739 }
740@@ -365,6 +371,10 @@
741 : Base(parent, x, y, w, h, button_background, rowtype) {
742 }
743
744+ void remove_entry(Entry const* entry) {
745+ Base::remove_entry(entry);
746+ }
747+
748 EntryRecord& add(Entry* const entry = 0, bool const select_this = false) {
749 return Base::add(entry, select_this);
750 }
751@@ -395,6 +405,10 @@
752 : Base(parent, x, y, w, h, button_background, rowtype) {
753 }
754
755+ void remove_entry(const Entry& entry) {
756+ Base::remove_entry(&const_cast<Entry&>(entry));
757+ }
758+
759 EntryRecord& add(const Entry& entry, bool const select_this = false) {
760 return Base::add(&const_cast<Entry&>(entry), select_this);
761 }
762@@ -429,6 +443,10 @@
763 : Base(parent, x, y, w, h, button_background, rowtype) {
764 }
765
766+ void remove_entry(Entry& entry) {
767+ Base::remove_entry(&entry);
768+ }
769+
770 EntryRecord& add(Entry& entry, bool const select_this = false) {
771 return Base::add(&entry, select_this);
772 }
773@@ -465,6 +483,10 @@
774 : Base(parent, x, y, w, h, button_background, rowtype) {
775 }
776
777+ void remove_entry(uintptr_t const entry) {
778+ Base::remove_entry(reinterpret_cast<void*>(entry));
779+ }
780+
781 EntryRecord& add(uintptr_t const entry, bool const select_this = false) {
782 return Base::add(reinterpret_cast<void*>(entry), select_this);
783 }
784
785=== modified file 'src/wui/CMakeLists.txt'
786--- src/wui/CMakeLists.txt 2017-11-20 13:50:51 +0000
787+++ src/wui/CMakeLists.txt 2018-04-28 09:26:35 +0000
788@@ -227,6 +227,8 @@
789 portdockwaresdisplay.h
790 productionsitewindow.cc
791 productionsitewindow.h
792+ seafaring_statistics_menu.cc
793+ seafaring_statistics_menu.h
794 shipwindow.cc
795 shipwindow.h
796 soldiercapacitycontrol.cc
797
798=== modified file 'src/wui/game_statistics_menu.cc'
799--- src/wui/game_statistics_menu.cc 2018-04-07 16:59:00 +0000
800+++ src/wui/game_statistics_menu.cc 2018-04-28 09:26:35 +0000
801@@ -27,6 +27,7 @@
802 #include "wui/building_statistics_menu.h"
803 #include "wui/general_statistics_menu.h"
804 #include "wui/interactive_player.h"
805+#include "wui/seafaring_statistics_menu.h"
806 #include "wui/stock_menu.h"
807 #include "wui/ware_statistics_menu.h"
808
809@@ -37,6 +38,7 @@
810 player_(plr),
811 windows_(windows),
812 box_(this, 0, 0, UI::Box::Horizontal, 0, 0, 5) {
813+ const bool is_seafaring = plr.egbase().mutable_map()->allows_seafaring();
814 add_button("wui/menus/menu_general_stats", "general_stats", _("General statistics"),
815 &windows_.general_stats);
816 add_button(
817@@ -44,8 +46,12 @@
818 add_button("wui/menus/menu_building_stats", "building_stats", _("Building statistics"),
819 &windows_.building_stats);
820 add_button("wui/menus/menu_stock", "stock", _("Stock"), &windows_.stock);
821+ if (is_seafaring) {
822+ add_button("wui/buildings/start_expedition", "seafaring_stats", _("Seafaring Statistics"),
823+ &windows_.seafaring_stats);
824+ }
825 box_.set_pos(Vector2i(10, 10));
826- box_.set_size((34 + 5) * 4, 34);
827+ box_.set_size((34 + 5) * (is_seafaring ? 5 : 4), 34);
828 set_inner_size(box_.get_w() + 20, box_.get_h() + 20);
829
830 windows_.general_stats.open_window = [this] {
831@@ -58,6 +64,11 @@
832 new BuildingStatisticsMenu(player_, windows_.building_stats);
833 };
834 // The stock window is defined in InteractivePlayer because of the keyboard shortcut.
835+ if (is_seafaring) {
836+ windows_.seafaring_stats.open_window = [this] {
837+ new SeafaringStatisticsMenu(player_, windows_.seafaring_stats);
838+ };
839+ }
840
841 if (get_usedefaultpos())
842 center_to_parent();
843
844=== modified file 'src/wui/interactive_gamebase.cc'
845--- src/wui/interactive_gamebase.cc 2018-04-27 20:12:08 +0000
846+++ src/wui/interactive_gamebase.cc 2018-04-28 09:26:35 +0000
847@@ -243,30 +243,33 @@
848 const Widelands::Map& map = game().map();
849 Widelands::Area<Widelands::FCoords> area(map.get_fcoords(get_sel_pos().node), 1);
850
851- if (!(area.field->nodecaps() & Widelands::MOVECAPS_SWIM))
852+ if (!(area.field->nodecaps() & Widelands::MOVECAPS_SWIM)) {
853 return false;
854+ }
855
856 std::vector<Widelands::Bob*> ships;
857- if (!map.find_bobs(area, &ships, Widelands::FindBobShip()))
858- return false;
859-
860- for (Widelands::Bob* temp_ship : ships) {
861- if (upcast(Widelands::Ship, ship, temp_ship)) {
862- if (can_see(ship->get_owner()->player_number())) {
863- UI::UniqueWindow::Registry& registry =
864- unique_windows().get_registry((boost::format("ship_%d") % ship->serial()).str());
865- registry.open_window = [this, &registry, ship] {
866- new ShipWindow(*this, registry, ship);
867- };
868- registry.create();
869- return true;
870- }
871- }
872- }
873-
874+ if (map.find_bobs(area, &ships, Widelands::FindBobShip())) {
875+ for (Widelands::Bob* ship : ships) {
876+ if (can_see(ship->owner().player_number())) {
877+ // FindBobShip should have returned only ships
878+ assert(ship->descr().type() == Widelands::MapObjectType::SHIP);
879+ show_ship_window(dynamic_cast<Widelands::Ship*>(ship));
880+ return true;
881+ }
882+ }
883+ }
884 return false;
885 }
886
887+void InteractiveGameBase::show_ship_window(Widelands::Ship* ship) {
888+ UI::UniqueWindow::Registry& registry =
889+ unique_windows().get_registry((boost::format("ship_%d") % ship->serial()).str());
890+ registry.open_window = [this, &registry, ship] {
891+ new ShipWindow(*this, registry, ship);
892+ };
893+ registry.create();
894+}
895+
896 void InteractiveGameBase::show_game_summary() {
897 if (game_summary_.window) {
898 game_summary_.window->set_visible(true);
899
900=== modified file 'src/wui/interactive_gamebase.h'
901--- src/wui/interactive_gamebase.h 2018-04-16 07:16:00 +0000
902+++ src/wui/interactive_gamebase.h 2018-04-28 09:26:35 +0000
903@@ -48,6 +48,7 @@
904 GeneralStatisticsMenu::Registry general_stats;
905 UI::UniqueWindow::Registry ware_stats;
906 UI::UniqueWindow::Registry stock;
907+ UI::UniqueWindow::Registry seafaring_stats;
908 };
909
910 InteractiveGameBase(Widelands::Game&,
911@@ -86,6 +87,7 @@
912 bool was_minimal);
913 UI::UniqueWindow* show_building_window(const Widelands::Coords& coords, bool avoid_fastclick);
914 bool try_show_ship_window();
915+ void show_ship_window(Widelands::Ship* ship);
916 bool is_multiplayer() {
917 return multiplayer_;
918 }
919
920=== modified file 'src/wui/interactive_player.cc'
921--- src/wui/interactive_player.cc 2018-04-07 16:59:00 +0000
922+++ src/wui/interactive_player.cc 2018-04-28 09:26:35 +0000
923@@ -51,6 +51,7 @@
924 #include "wui/game_options_menu.h"
925 #include "wui/game_statistics_menu.h"
926 #include "wui/general_statistics_menu.h"
927+#include "wui/seafaring_statistics_menu.h"
928 #include "wui/stock_menu.h"
929 #include "wui/tribal_encyclopedia.h"
930 #include "wui/ware_statistics_menu.h"
931@@ -459,6 +460,16 @@
932 }
933 return true;
934
935+ case SDLK_e:
936+ if (game().map().allows_seafaring()) {
937+ if (main_windows_.seafaring_stats.window == nullptr) {
938+ new SeafaringStatisticsMenu(*this, main_windows_.seafaring_stats);
939+ } else {
940+ main_windows_.seafaring_stats.toggle();
941+ }
942+ }
943+ return true;
944+
945 case SDLK_s:
946 if (code.mod & (KMOD_LCTRL | KMOD_RCTRL))
947 new GameMainMenuSaveGame(*this, main_windows_.savegame);
948
949=== added file 'src/wui/seafaring_statistics_menu.cc'
950--- src/wui/seafaring_statistics_menu.cc 1970-01-01 00:00:00 +0000
951+++ src/wui/seafaring_statistics_menu.cc 2018-04-28 09:26:35 +0000
952@@ -0,0 +1,580 @@
953+/*
954+ * Copyright (C) 2017-2018 by the Widelands Development Team
955+ *
956+ * This program is free software; you can redistribute it and/or
957+ * modify it under the terms of the GNU General Public License
958+ * as published by the Free Software Foundation; either version 2
959+ * of the License, or (at your option) any later version.
960+ *
961+ * This program is distributed in the hope that it will be useful,
962+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
963+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
964+ * GNU General Public License for more details.
965+ *
966+ * You should have received a copy of the GNU General Public License
967+ * along with this program; if not, write to the Free Software
968+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
969+ *
970+ */
971+
972+#include "wui/seafaring_statistics_menu.h"
973+
974+#include <memory>
975+
976+#include <boost/bind.hpp>
977+#include <boost/format.hpp>
978+
979+#include "economy/fleet.h"
980+#include "graphic/graphic.h"
981+#include "logic/game.h"
982+#include "logic/player.h"
983+#include "logic/playercommand.h"
984+#include "ui_basic/box.h"
985+#include "wui/interactive_player.h"
986+#include "wui/shipwindow.h"
987+#include "wui/watchwindow.h"
988+
989+inline InteractivePlayer& SeafaringStatisticsMenu::iplayer() const {
990+ return dynamic_cast<InteractivePlayer&>(*get_parent());
991+}
992+
993+constexpr int kPadding = 5;
994+constexpr int kButtonSize = 34;
995+
996+SeafaringStatisticsMenu::SeafaringStatisticsMenu(InteractivePlayer& plr,
997+ UI::UniqueWindow::Registry& registry)
998+ : UI::UniqueWindow(&plr, "seafaring_statistics", &registry, 355, 375, _("Seafaring Statistics")),
999+ main_box_(this, kPadding, kPadding, UI::Box::Vertical, get_inner_w(), get_inner_h(), kPadding),
1000+ filter_box_(
1001+ &main_box_, 0, 0, UI::Box::Horizontal, get_inner_w() - 2 * kPadding, kButtonSize, kPadding),
1002+ idle_btn_(&filter_box_,
1003+ "filter_ship_idle",
1004+ 0,
1005+ 0,
1006+ kButtonSize,
1007+ kButtonSize,
1008+ g_gr->images().get("images/ui_basic/but0.png"),
1009+ status_to_image(ShipFilterStatus::kIdle)),
1010+ waiting_btn_(&filter_box_,
1011+ "filter_ship_waiting",
1012+ 0,
1013+ 0,
1014+ kButtonSize,
1015+ kButtonSize,
1016+ g_gr->images().get("images/ui_basic/but0.png"),
1017+ status_to_image(ShipFilterStatus::kExpeditionWaiting)),
1018+ scouting_btn_(&filter_box_,
1019+ "filter_ship_scouting",
1020+ 0,
1021+ 0,
1022+ kButtonSize,
1023+ kButtonSize,
1024+ g_gr->images().get("images/ui_basic/but0.png"),
1025+ status_to_image(ShipFilterStatus::kExpeditionScouting)),
1026+ portspace_btn_(&filter_box_,
1027+ "filter_ship_portspace",
1028+ 0,
1029+ 0,
1030+ kButtonSize,
1031+ kButtonSize,
1032+ g_gr->images().get("images/ui_basic/but0.png"),
1033+ status_to_image(ShipFilterStatus::kExpeditionPortspaceFound)),
1034+ shipping_btn_(&filter_box_,
1035+ "filter_ship_transporting",
1036+ 0,
1037+ 0,
1038+ kButtonSize,
1039+ kButtonSize,
1040+ g_gr->images().get("images/ui_basic/but0.png"),
1041+ status_to_image(ShipFilterStatus::kShipping)),
1042+ ship_filter_(ShipFilterStatus::kAll),
1043+ navigation_box_(
1044+ &main_box_, 0, 0, UI::Box::Horizontal, get_inner_w() - 2 * kPadding, kButtonSize, kPadding),
1045+ watchbtn_(&navigation_box_,
1046+ "seafaring_stats_watch_button",
1047+ 0,
1048+ 0,
1049+ kButtonSize,
1050+ kButtonSize,
1051+ g_gr->images().get("images/ui_basic/but2.png"),
1052+ g_gr->images().get("images/wui/menus/menu_watch_follow.png"),
1053+ (boost::format(_("%1% (Hotkey: %2%)")) %
1054+ /** TRANSLATORS: Tooltip in the seafaring statistics window */
1055+ _("Watch the selected ship") %
1056+ pgettext("hotkey", "W"))
1057+ .str()),
1058+ openwindowbtn_(&navigation_box_,
1059+ "seafaring_stats_watch_button",
1060+ 0,
1061+ 0,
1062+ kButtonSize,
1063+ kButtonSize,
1064+ g_gr->images().get("images/ui_basic/but2.png"),
1065+ g_gr->images().get("images/ui_basic/fsel.png"),
1066+ (boost::format("%s<br>%s") %
1067+ (boost::format(pgettext("hotkey_description", "%1%: %2%")) %
1068+ pgettext("hotkey", "O") %
1069+ /** TRANSLATORS: Tooltip in the seafaring statistics window */
1070+ _("Open the selected ship’s window")) %
1071+ (boost::format(pgettext("hotkey_description", "%1%: %2%")) %
1072+ pgettext("hotkey", "CTRL + O") %
1073+ /** TRANSLATORS: Tooltip in the seafaring statistics window */
1074+ _("Go to the selected ship and open its window")))
1075+ .str()),
1076+ centerviewbtn_(&navigation_box_,
1077+ "seafaring_stats_center_main_mapview_button",
1078+ 0,
1079+ 0,
1080+ kButtonSize,
1081+ kButtonSize,
1082+ g_gr->images().get("images/ui_basic/but2.png"),
1083+ g_gr->images().get("images/wui/ship/menu_ship_goto.png"),
1084+ (boost::format(_("%1% (Hotkey: %2%)")) %
1085+ /** TRANSLATORS: Tooltip in the seafaring statistics window */
1086+ _("Center the map on the selected ship") %
1087+ pgettext("hotkey", "G"))
1088+ .str()),
1089+ table_(&main_box_,
1090+ 0,
1091+ 0,
1092+ get_inner_w() - 2 * kPadding,
1093+ 100,
1094+ g_gr->images().get("images/ui_basic/but1.png")) {
1095+
1096+ const Widelands::TribeDescr& tribe = iplayer().player().tribe();
1097+ colony_icon_ = tribe.get_worker_descr(tribe.builder())->icon();
1098+
1099+ // Buttons for ship states
1100+ main_box_.add(&filter_box_, UI::Box::Resizing::kFullSize);
1101+ filter_box_.add(&idle_btn_);
1102+ filter_box_.add(&shipping_btn_);
1103+ filter_box_.add(&waiting_btn_);
1104+ filter_box_.add(&scouting_btn_);
1105+ filter_box_.add(&portspace_btn_);
1106+
1107+ main_box_.add(&table_, UI::Box::Resizing::kExpandBoth);
1108+
1109+ // Navigation buttons
1110+ main_box_.add(&navigation_box_, UI::Box::Resizing::kFullSize);
1111+ navigation_box_.add(&watchbtn_);
1112+ navigation_box_.add_inf_space();
1113+ navigation_box_.add(&openwindowbtn_);
1114+ navigation_box_.add(&centerviewbtn_);
1115+ main_box_.set_size(get_inner_w() - 2 * kPadding, get_inner_h() - 2 * kPadding);
1116+
1117+ // Configure actions
1118+ idle_btn_.sigclicked.connect(
1119+ boost::bind(&SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kIdle));
1120+ shipping_btn_.sigclicked.connect(
1121+ boost::bind(&SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kShipping));
1122+ waiting_btn_.sigclicked.connect(boost::bind(
1123+ &SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kExpeditionWaiting));
1124+ scouting_btn_.sigclicked.connect(boost::bind(
1125+ &SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kExpeditionScouting));
1126+ portspace_btn_.sigclicked.connect(boost::bind(
1127+ &SeafaringStatisticsMenu::filter_ships, this, ShipFilterStatus::kExpeditionPortspaceFound));
1128+ ship_filter_ = ShipFilterStatus::kAll;
1129+ set_filter_ships_tooltips();
1130+
1131+ watchbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::watch_ship, this));
1132+ openwindowbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::open_ship_window, this));
1133+ centerviewbtn_.sigclicked.connect(boost::bind(&SeafaringStatisticsMenu::center_view, this));
1134+
1135+ // Configure table
1136+ table_.selected.connect(boost::bind(&SeafaringStatisticsMenu::selected, this));
1137+ table_.double_clicked.connect(boost::bind(&SeafaringStatisticsMenu::double_clicked, this));
1138+ table_.add_column(
1139+ 0, pgettext("ship", "Name"), "", UI::Align::kLeft, UI::TableColumnType::kFlexible);
1140+ table_.add_column(200, pgettext("ship", "Status"));
1141+ table_.set_sort_column(ColName);
1142+ fill_table();
1143+
1144+ set_can_focus(true);
1145+ set_thinks(false);
1146+ table_.focus();
1147+
1148+ shipnotes_subscriber_ =
1149+ Notifications::subscribe<Widelands::NoteShip>([this](const Widelands::NoteShip& note) {
1150+ if (iplayer().get_player() == note.ship->get_owner()) {
1151+ switch (note.action) {
1152+ case Widelands::NoteShip::Action::kDestinationChanged:
1153+ case Widelands::NoteShip::Action::kWaitingForCommand:
1154+ case Widelands::NoteShip::Action::kGained:
1155+ assert(note.ship != nullptr);
1156+ update_ship(*note.ship);
1157+ break;
1158+ case Widelands::NoteShip::Action::kLost:
1159+ remove_ship(note.ship->serial());
1160+ break;
1161+ default:
1162+ NEVER_HERE();
1163+ }
1164+ }
1165+ });
1166+}
1167+
1168+const std::string
1169+SeafaringStatisticsMenu::status_to_string(SeafaringStatisticsMenu::ShipFilterStatus status) const {
1170+ switch (status) {
1171+ case SeafaringStatisticsMenu::ShipFilterStatus::kIdle:
1172+ return pgettext("ship_state", "Idle");
1173+ case SeafaringStatisticsMenu::ShipFilterStatus::kShipping:
1174+ return pgettext("ship_state", "Shipping");
1175+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionWaiting:
1176+ return pgettext("ship_state", "Waiting");
1177+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionScouting:
1178+ return pgettext("ship_state", "Scouting");
1179+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionPortspaceFound:
1180+ return pgettext("ship_state", "Port Space Found");
1181+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionColonizing:
1182+ return pgettext("ship_state", "Founding a Colony");
1183+ case SeafaringStatisticsMenu::ShipFilterStatus::kAll:
1184+ return "All"; // The user shouldn't see this, so we don't localize
1185+ default:
1186+ NEVER_HERE();
1187+ }
1188+}
1189+
1190+const Image*
1191+SeafaringStatisticsMenu::status_to_image(SeafaringStatisticsMenu::ShipFilterStatus status) const {
1192+ std::string filename = "";
1193+ switch (status) {
1194+ case SeafaringStatisticsMenu::ShipFilterStatus::kIdle:
1195+ filename = "images/wui/stats/ship_stats_idle.png";
1196+ break;
1197+ case SeafaringStatisticsMenu::ShipFilterStatus::kShipping:
1198+ filename = "images/wui/stats/ship_stats_shipping.png";
1199+ break;
1200+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionWaiting:
1201+ filename = "images/wui/buildings/start_expedition.png";
1202+ break;
1203+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionScouting:
1204+ filename = "images/wui/ship/ship_explore_island_cw.png";
1205+ break;
1206+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionPortspaceFound:
1207+ filename = "images/wui/ship/ship_construct_port_space.png";
1208+ break;
1209+ case SeafaringStatisticsMenu::ShipFilterStatus::kExpeditionColonizing:
1210+ return colony_icon_;
1211+ case SeafaringStatisticsMenu::ShipFilterStatus::kAll:
1212+ filename = "images/wui/ship/ship_scout_ne.png";
1213+ break;
1214+ default:
1215+ NEVER_HERE();
1216+ }
1217+ return g_gr->images().get(filename);
1218+}
1219+
1220+std::unique_ptr<const SeafaringStatisticsMenu::ShipInfo>
1221+SeafaringStatisticsMenu::create_shipinfo(const Widelands::Ship& ship) const {
1222+ const Widelands::Ship::ShipStates state = ship.get_ship_state();
1223+ ShipFilterStatus status = ShipFilterStatus::kAll;
1224+ switch (state) {
1225+ case Widelands::Ship::ShipStates::kTransport:
1226+ if (ship.get_destination(iplayer().game()) != nullptr) {
1227+ status = ShipFilterStatus::kShipping;
1228+ } else {
1229+ status = ShipFilterStatus::kIdle;
1230+ }
1231+ break;
1232+ case Widelands::Ship::ShipStates::kExpeditionWaiting:
1233+ status = ShipFilterStatus::kExpeditionWaiting;
1234+ break;
1235+ case Widelands::Ship::ShipStates::kExpeditionScouting:
1236+ status = ShipFilterStatus::kExpeditionScouting;
1237+ break;
1238+ case Widelands::Ship::ShipStates::kExpeditionPortspaceFound:
1239+ status = ShipFilterStatus::kExpeditionPortspaceFound;
1240+ break;
1241+ case Widelands::Ship::ShipStates::kExpeditionColonizing:
1242+ status = ShipFilterStatus::kExpeditionColonizing;
1243+ break;
1244+ case Widelands::Ship::ShipStates::kSinkRequest:
1245+ case Widelands::Ship::ShipStates::kSinkAnimation:
1246+ status = ShipFilterStatus::kAll;
1247+ }
1248+ return std::unique_ptr<const ShipInfo>(new ShipInfo(ship.get_shipname(), status, ship.serial()));
1249+}
1250+
1251+void SeafaringStatisticsMenu::set_entry_record(UI::Table<uintptr_t>::EntryRecord* er,
1252+ const ShipInfo& info) {
1253+ if (info.status != ShipFilterStatus::kAll) {
1254+ er->set_string(ColName, info.name);
1255+ er->set_picture(ColStatus, status_to_image(info.status), status_to_string(info.status));
1256+ }
1257+}
1258+
1259+Widelands::Ship* SeafaringStatisticsMenu::serial_to_ship(Widelands::Serial serial) const {
1260+ Widelands::MapObject* obj = iplayer().game().objects().get_object(serial);
1261+ assert(obj->descr().type() == Widelands::MapObjectType::SHIP);
1262+ upcast(Widelands::Ship, ship, obj);
1263+ return ship;
1264+}
1265+
1266+void SeafaringStatisticsMenu::update_ship(const Widelands::Ship& ship) {
1267+ assert(iplayer().get_player() == ship.get_owner());
1268+ std::unique_ptr<const ShipInfo> info = create_shipinfo(ship);
1269+ // Remove ships that don't satisfy the filter
1270+ if (ship_filter_ != ShipFilterStatus::kAll && !satisfies_filter(*info, ship_filter_)) {
1271+ remove_ship(info->serial);
1272+ return;
1273+ }
1274+ // Try to find the ship in the table
1275+ if (data_.count(info->serial) == 1) {
1276+ const ShipInfo* old_info = data_[info->serial].get();
1277+ if (info->status != old_info->status) {
1278+ // The status has changed - we need an update
1279+ UI::Table<uintptr_t>::EntryRecord* er = table_.find(info->serial);
1280+ set_entry_record(er, *info);
1281+ data_[info->serial] = std::move(info);
1282+ }
1283+ } else {
1284+ // This is a new ship or it was filtered away before
1285+ UI::Table<uintptr_t>::EntryRecord& er = table_.add(info->serial);
1286+ set_entry_record(&er, *info);
1287+ data_.insert(std::make_pair(info->serial, std::move(info)));
1288+ }
1289+ table_.sort();
1290+ update_button_states();
1291+}
1292+
1293+void SeafaringStatisticsMenu::remove_ship(Widelands::Serial serial) {
1294+ if (data_.count(serial) == 1) {
1295+ table_.remove_entry(serial);
1296+ data_.erase(data_.find(serial));
1297+ if (!table_.empty() && !table_.has_selection()) {
1298+ table_.select(0);
1299+ }
1300+ update_button_states();
1301+ }
1302+}
1303+
1304+void SeafaringStatisticsMenu::update_entry_record(UI::Table<uintptr_t>::EntryRecord& er,
1305+ const ShipInfo& info) {
1306+ er.set_picture(ColStatus, status_to_image(info.status), status_to_string(info.status));
1307+}
1308+
1309+void SeafaringStatisticsMenu::selected() {
1310+ update_button_states();
1311+}
1312+
1313+void SeafaringStatisticsMenu::double_clicked() {
1314+ if (table_.has_selection()) {
1315+ center_view();
1316+ }
1317+}
1318+
1319+void SeafaringStatisticsMenu::update_button_states() {
1320+ centerviewbtn_.set_enabled(table_.has_selection());
1321+ openwindowbtn_.set_enabled(table_.has_selection());
1322+ watchbtn_.set_enabled(table_.has_selection());
1323+}
1324+
1325+bool SeafaringStatisticsMenu::handle_key(bool down, SDL_Keysym code) {
1326+ if (down) {
1327+ switch (code.sym) {
1328+ // Don't forget to change the tooltips if any of these get reassigned
1329+ case SDLK_g:
1330+ center_view();
1331+ return true;
1332+ case SDLK_o:
1333+ open_ship_window();
1334+ return true;
1335+ case SDLK_w:
1336+ watch_ship();
1337+ return true;
1338+ case SDLK_0:
1339+ if (code.mod & KMOD_ALT) {
1340+ filter_ships(ShipFilterStatus::kAll);
1341+ return true;
1342+ }
1343+ return false;
1344+ case SDLK_1:
1345+ if (code.mod & KMOD_ALT) {
1346+ filter_ships(ShipFilterStatus::kIdle);
1347+ return true;
1348+ }
1349+ return false;
1350+ case SDLK_2:
1351+ if (code.mod & KMOD_ALT) {
1352+ filter_ships(ShipFilterStatus::kShipping);
1353+ return true;
1354+ }
1355+ return false;
1356+ case SDLK_3:
1357+ if (code.mod & KMOD_ALT) {
1358+ filter_ships(ShipFilterStatus::kExpeditionWaiting);
1359+ return true;
1360+ }
1361+ return false;
1362+ case SDLK_4:
1363+ if (code.mod & KMOD_ALT) {
1364+ filter_ships(ShipFilterStatus::kExpeditionScouting);
1365+ return true;
1366+ }
1367+ return false;
1368+ case SDLK_5:
1369+ if (code.mod & KMOD_ALT) {
1370+ filter_ships(ShipFilterStatus::kExpeditionPortspaceFound);
1371+ return true;
1372+ }
1373+ return false;
1374+ case SDL_SCANCODE_KP_PERIOD:
1375+ case SDLK_KP_PERIOD:
1376+ if (code.mod & KMOD_NUM)
1377+ break;
1378+ /* no break */
1379+ default:
1380+ break; // not handled
1381+ }
1382+ }
1383+
1384+ return table_.handle_key(down, code);
1385+}
1386+
1387+void SeafaringStatisticsMenu::center_view() {
1388+ if (table_.has_selection()) {
1389+ Widelands::Ship* ship = serial_to_ship(table_.get_selected());
1390+ iplayer().map_view()->scroll_to_field(ship->get_position(), MapView::Transition::Smooth);
1391+ }
1392+}
1393+
1394+void SeafaringStatisticsMenu::watch_ship() {
1395+ if (table_.has_selection()) {
1396+ Widelands::Ship* ship = serial_to_ship(table_.get_selected());
1397+ WatchWindow* window = show_watch_window(iplayer(), ship->get_position());
1398+ window->follow(ship);
1399+ }
1400+}
1401+
1402+void SeafaringStatisticsMenu::open_ship_window() {
1403+ if (table_.has_selection()) {
1404+ // Move to ship if CTRL is prssed
1405+ if (SDL_GetModState() & KMOD_CTRL) {
1406+ center_view();
1407+ }
1408+ Widelands::Ship* ship = serial_to_ship(table_.get_selected());
1409+ iplayer().show_ship_window(ship);
1410+ }
1411+}
1412+
1413+void SeafaringStatisticsMenu::filter_ships(ShipFilterStatus status) {
1414+ switch (status) {
1415+ case ShipFilterStatus::kExpeditionWaiting:
1416+ toggle_filter_ships_button(waiting_btn_, status);
1417+ break;
1418+ case ShipFilterStatus::kExpeditionScouting:
1419+ toggle_filter_ships_button(scouting_btn_, status);
1420+ break;
1421+ // We're grouping the "colonizing" status with the port space.
1422+ case ShipFilterStatus::kExpeditionColonizing:
1423+ case ShipFilterStatus::kExpeditionPortspaceFound:
1424+ toggle_filter_ships_button(portspace_btn_, status);
1425+ break;
1426+ case ShipFilterStatus::kShipping:
1427+ toggle_filter_ships_button(shipping_btn_, status);
1428+ break;
1429+ case ShipFilterStatus::kIdle:
1430+ toggle_filter_ships_button(idle_btn_, status);
1431+ break;
1432+ case ShipFilterStatus::kAll:
1433+ set_filter_ships_tooltips();
1434+ ship_filter_ = ShipFilterStatus::kAll;
1435+ waiting_btn_.set_perm_pressed(false);
1436+ scouting_btn_.set_perm_pressed(false);
1437+ portspace_btn_.set_perm_pressed(false);
1438+ shipping_btn_.set_perm_pressed(false);
1439+ idle_btn_.set_perm_pressed(false);
1440+ break;
1441+ }
1442+ fill_table();
1443+}
1444+
1445+void SeafaringStatisticsMenu::toggle_filter_ships_button(UI::Button& button,
1446+ ShipFilterStatus status) {
1447+ set_filter_ships_tooltips();
1448+ if (button.style() == UI::Button::Style::kPermpressed) {
1449+ button.set_perm_pressed(false);
1450+ ship_filter_ = ShipFilterStatus::kAll;
1451+ } else {
1452+ waiting_btn_.set_perm_pressed(false);
1453+ scouting_btn_.set_perm_pressed(false);
1454+ portspace_btn_.set_perm_pressed(false);
1455+ shipping_btn_.set_perm_pressed(false);
1456+ idle_btn_.set_perm_pressed(false);
1457+ button.set_perm_pressed(true);
1458+ ship_filter_ = status;
1459+
1460+ /** TRANSLATORS: %1% is a tooltip, %2% is the corresponding hotkey */
1461+ button.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
1462+ /** TRANSLATORS: Tooltip in the messages window */
1463+ % _("Show all ships") % pgettext("hotkey", "Alt + 0"))
1464+ .str());
1465+ }
1466+}
1467+
1468+void SeafaringStatisticsMenu::set_filter_ships_tooltips() {
1469+
1470+ idle_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
1471+ /** TRANSLATORS: Tooltip in the messages window */
1472+ % _("Show idle ships") % pgettext("hotkey", "Alt + 1"))
1473+ .str());
1474+ shipping_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
1475+ /** TRANSLATORS: Tooltip in the messages window */
1476+ % _("Show ships shipping wares and workers") %
1477+ pgettext("hotkey", "Alt + 2"))
1478+ .str());
1479+ waiting_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
1480+ /** TRANSLATORS: Tooltip in the messages window */
1481+ % _("Show waiting expeditions") % pgettext("hotkey", "Alt + 3"))
1482+ .str());
1483+ scouting_btn_.set_tooltip((boost::format(_("%1% (Hotkey: %2%)"))
1484+ /** TRANSLATORS: Tooltip in the messages window */
1485+ % _("Show scouting expeditions") % pgettext("hotkey", "Alt + 4"))
1486+ .str());
1487+ portspace_btn_.set_tooltip(
1488+ (boost::format(_("%1% (Hotkey: %2%)"))
1489+ /** TRANSLATORS: Tooltip in the messages window */
1490+ % _("Show expeditions that have found a port space or are founding a colony") %
1491+ pgettext("hotkey", "Alt + 5"))
1492+ .str());
1493+}
1494+
1495+bool SeafaringStatisticsMenu::satisfies_filter(const ShipInfo& info, ShipFilterStatus filter) {
1496+ return filter == info.status || (filter == ShipFilterStatus::kExpeditionPortspaceFound &&
1497+ info.status == ShipFilterStatus::kExpeditionColonizing);
1498+}
1499+
1500+void SeafaringStatisticsMenu::fill_table() {
1501+ const Widelands::Serial last_selection =
1502+ table_.has_selection() ? table_.get_selected() : Widelands::INVALID_INDEX;
1503+ table_.clear();
1504+ data_.clear();
1505+ // Disable buttons while we update the data
1506+ update_button_states();
1507+ for (const auto& serial : iplayer().player().ships()) {
1508+ Widelands::Ship* ship = serial_to_ship(serial);
1509+ assert(ship != nullptr);
1510+ assert(iplayer().get_player() == ship->get_owner());
1511+ std::unique_ptr<const ShipInfo> info = create_shipinfo(*ship);
1512+ if (info->status != ShipFilterStatus::kAll) {
1513+ if (ship_filter_ == ShipFilterStatus::kAll || satisfies_filter(*info, ship_filter_)) {
1514+ UI::Table<uintptr_t const>::EntryRecord& er =
1515+ table_.add(serial, serial == last_selection);
1516+ set_entry_record(&er, *info);
1517+ data_.insert(std::make_pair(serial, std::move(info)));
1518+ }
1519+ }
1520+ }
1521+
1522+ if (!table_.empty()) {
1523+ table_.sort();
1524+ if (!table_.has_selection()) {
1525+ // This will take care of the button state too
1526+ table_.select(0);
1527+ }
1528+ } else {
1529+ // Fix column width. Because no entries were added, the table didn't layout itself
1530+ table_.layout();
1531+ }
1532+}
1533
1534=== added file 'src/wui/seafaring_statistics_menu.h'
1535--- src/wui/seafaring_statistics_menu.h 1970-01-01 00:00:00 +0000
1536+++ src/wui/seafaring_statistics_menu.h 2018-04-28 09:26:35 +0000
1537@@ -0,0 +1,159 @@
1538+/*
1539+ * Copyright (C) 2017-2018 by the Widelands Development Team
1540+ *
1541+ * This program is free software; you can redistribute it and/or
1542+ * modify it under the terms of the GNU General Public License
1543+ * as published by the Free Software Foundation; either version 2
1544+ * of the License, or (at your option) any later version.
1545+ *
1546+ * This program is distributed in the hope that it will be useful,
1547+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1548+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1549+ * GNU General Public License for more details.
1550+ *
1551+ * You should have received a copy of the GNU General Public License
1552+ * along with this program; if not, write to the Free Software
1553+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1554+ *
1555+ */
1556+
1557+#ifndef WL_WUI_SEAFARING_STATISTICS_MENU_H
1558+#define WL_WUI_SEAFARING_STATISTICS_MENU_H
1559+
1560+#include <memory>
1561+#include <unordered_map>
1562+
1563+#include "base/i18n.h"
1564+#include "logic/map_objects/tribes/ship.h"
1565+#include "notifications/notifications.h"
1566+#include "ui_basic/box.h"
1567+#include "ui_basic/button.h"
1568+#include "ui_basic/table.h"
1569+#include "ui_basic/unique_window.h"
1570+
1571+class InteractivePlayer;
1572+
1573+/// Shows a list of the ships owned by the interactive player with filtering and navigation options.
1574+struct SeafaringStatisticsMenu : public UI::UniqueWindow {
1575+ SeafaringStatisticsMenu(InteractivePlayer&, UI::UniqueWindow::Registry&);
1576+
1577+private:
1578+ /// For identifying the columns in the table.
1579+ enum Cols { ColName, ColStatus };
1580+ /**
1581+ * A list of ship status that we can filter for. This differs a bit from that the Widelands::Ship
1582+ * class has, so we define our own.
1583+ * */
1584+ enum class ShipFilterStatus {
1585+ kIdle,
1586+ kShipping,
1587+ kExpeditionWaiting,
1588+ kExpeditionScouting,
1589+ kExpeditionPortspaceFound,
1590+ kExpeditionColonizing,
1591+ kAll
1592+ };
1593+
1594+ /// Returns the localized strings that we use to display the 'status' in the table.
1595+ const std::string status_to_string(ShipFilterStatus status) const;
1596+ /// Returns the icon that we use to represent the 'status' in the table and on the filter
1597+ /// buttons.
1598+ const Image* status_to_image(ShipFilterStatus status) const;
1599+
1600+ /// The dataset that we need to display ships in the table.
1601+ struct ShipInfo {
1602+ ShipInfo(const std::string& init_name,
1603+ const ShipFilterStatus init_status,
1604+ const Widelands::Serial init_serial)
1605+ : name(init_name), status(init_status), serial(init_serial) {
1606+ }
1607+ ShipInfo() : ShipInfo("", ShipFilterStatus::kAll, 0) {
1608+ }
1609+ ShipInfo(const ShipInfo& other) : ShipInfo(other.name, other.status, other.serial) {
1610+ }
1611+ bool operator==(const ShipInfo& other) const {
1612+ return serial == other.serial;
1613+ }
1614+ bool operator<(const ShipInfo& other) const {
1615+ return serial < other.serial;
1616+ }
1617+
1618+ const std::string name;
1619+ ShipFilterStatus status;
1620+ const Widelands::Serial serial;
1621+ };
1622+
1623+ /// Creates our dataset from a 'ship'.
1624+ std::unique_ptr<const ShipInfo> create_shipinfo(const Widelands::Ship& ship) const;
1625+ /// Uses the 'serial' to identify and get a ship from the game.
1626+ Widelands::Ship* serial_to_ship(Widelands::Serial serial) const;
1627+
1628+ /// Convenience function to upcast the panel's parent.
1629+ InteractivePlayer& iplayer() const;
1630+
1631+ /// A ship was selected, so enable the navigation buttons.
1632+ void selected();
1633+ /// A ship was double clicked. Centers main view on ship.
1634+ void double_clicked();
1635+ /// Handle filter and navigation hotkeys
1636+ bool handle_key(bool down, SDL_Keysym code) override;
1637+
1638+ /// Enables the navigation buttons if a ship is selected, disables them otherwise.
1639+ void update_button_states();
1640+ /// Center the mapview on the currently selected ship.
1641+ void center_view();
1642+ /// Follow the selected ship in a watch window.
1643+ void watch_ship();
1644+ /// Open the currently selected ship's window. If CTRL is pressed, center the mapview on it.
1645+ void open_ship_window();
1646+
1647+ /**
1648+ * Updates the status for the ship. If the ship is new and satisfies the 'ship_filter_',
1649+ * adds it to the table and data. If it doesn't satisfy the 'ship_filter_', removes the ship
1650+ * instead.
1651+ * */
1652+ void update_ship(const Widelands::Ship&);
1653+ /// If we listed the ship, remove it from table and data.
1654+ void remove_ship(Widelands::Serial serial);
1655+ /// Sets the contents for the entry record in the table.
1656+ void set_entry_record(UI::Table<uintptr_t>::EntryRecord*, const ShipInfo& info);
1657+ /// Updates the ship status display in the table.
1658+ void update_entry_record(UI::Table<uintptr_t>::EntryRecord& er, const ShipInfo&);
1659+ /// Rebuilds data and table with all ships that satisfy the current 'ship_filter_'.
1660+ void fill_table();
1661+
1662+ /// Show only the ships that have the given status. Toggle the appropriate buttons.
1663+ void filter_ships(ShipFilterStatus status);
1664+ /// Helper for filter_ships
1665+ void toggle_filter_ships_button(UI::Button&, ShipFilterStatus);
1666+ /// Helper for filter_ships
1667+ void set_filter_ships_tooltips();
1668+
1669+ /// We group colonizing status with port space found. Anything else needs to have an identical
1670+ /// status.
1671+ bool satisfies_filter(const ShipInfo& info, ShipFilterStatus filter);
1672+
1673+ const Image* colony_icon_;
1674+ UI::Box main_box_;
1675+ // Buttons for ship states
1676+ UI::Box filter_box_;
1677+ UI::Button idle_btn_;
1678+ UI::Button waiting_btn_;
1679+ UI::Button scouting_btn_;
1680+ UI::Button portspace_btn_;
1681+ UI::Button shipping_btn_;
1682+ ShipFilterStatus ship_filter_;
1683+ // Navigation buttons
1684+ UI::Box navigation_box_;
1685+ UI::Button watchbtn_;
1686+ UI::Button openwindowbtn_;
1687+ UI::Button centerviewbtn_;
1688+
1689+ // Data
1690+ UI::Table<uintptr_t> table_;
1691+ std::unordered_map<Widelands::Serial, std::unique_ptr<const ShipInfo>> data_;
1692+
1693+ std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> shipnotes_subscriber_;
1694+};
1695+
1696+#endif // end of include guard: WL_WUI_SEAFARING_STATISTICS_MENU_H
1697
1698=== modified file 'src/wui/shipwindow.cc'
1699--- src/wui/shipwindow.cc 2018-04-27 16:46:34 +0000
1700+++ src/wui/shipwindow.cc 2018-04-28 09:26:35 +0000
1701@@ -46,7 +46,7 @@
1702 static const char pic_scout_e[] = "images/wui/ship/ship_scout_e.png";
1703 static const char pic_scout_sw[] = "images/wui/ship/ship_scout_sw.png";
1704 static const char pic_scout_se[] = "images/wui/ship/ship_scout_se.png";
1705-static const char pic_construct_port[] = "images/wui/editor/fsel_editor_set_port_space.png";
1706+static const char pic_construct_port[] = "images/wui/ship/ship_construct_port_space.png";
1707
1708 constexpr int kPadding = 5;
1709 } // namespace
1710@@ -158,38 +158,29 @@
1711 move_out_of_the_way();
1712 warp_mouse_to_fastclick_panel();
1713
1714- shipnotes_subscriber_ = Notifications::subscribe<Widelands::NoteShipWindow>(
1715- [this](const Widelands::NoteShipWindow& note) {
1716- if (note.serial == ship_.serial()) {
1717- switch (note.action) {
1718- // Unable to cancel the expedition
1719- case Widelands::NoteShipWindow::Action::kNoPortLeft: {
1720- Widelands::Ship* note_ship = ship_.get(igbase_.egbase());
1721- if (note_ship == nullptr) {
1722- return;
1723- }
1724- if (upcast(InteractiveGameBase, igamebase,
1725- note_ship->get_owner()->egbase().get_ibase())) {
1726- if (igamebase->can_act(note_ship->get_owner()->player_number())) {
1727- UI::WLMessageBox messagebox(
1728- get_parent(),
1729- /** TRANSLATORS: Window label when an expedition can't be canceled */
1730- _("Cancel Expedition"), _("This expedition can’t be canceled, because the "
1731- "ship has no port to return to."),
1732- UI::WLMessageBox::MBoxType::kOk);
1733- messagebox.run<UI::Panel::Returncodes>();
1734- }
1735- }
1736- } break;
1737- // The ship is no more
1738- case Widelands::NoteShipWindow::Action::kClose:
1739- // Stop this from thinking to avoid segfaults
1740- set_thinks(false);
1741- die();
1742- break;
1743- }
1744- }
1745- });
1746+ shipnotes_subscriber_ = Notifications::subscribe<Widelands::NoteShip>([this](
1747+ const Widelands::NoteShip& note) {
1748+ if (note.ship->serial() == ship_.serial()) {
1749+ switch (note.action) {
1750+ // Unable to cancel the expedition
1751+ case Widelands::NoteShip::Action::kNoPortLeft:
1752+ no_port_error_message();
1753+ break;
1754+ // The ship is no more
1755+ case Widelands::NoteShip::Action::kLost:
1756+ // Stop this from thinking to avoid segfaults
1757+ set_thinks(false);
1758+ die();
1759+ break;
1760+ // If the ship state has changed, e.g. expedition started or scouting direction changed,
1761+ // think() will take care of it.
1762+ case Widelands::NoteShip::Action::kDestinationChanged:
1763+ case Widelands::NoteShip::Action::kWaitingForCommand:
1764+ case Widelands::NoteShip::Action::kGained:
1765+ break;
1766+ }
1767+ }
1768+ });
1769
1770 // Init button visibility
1771 navigation_box_height_ = navigation_box_.get_h();
1772@@ -215,6 +206,24 @@
1773 }
1774 }
1775
1776+void ShipWindow::no_port_error_message() {
1777+ Widelands::Ship* ship = ship_.get(igbase_.egbase());
1778+ if (ship == nullptr) {
1779+ return;
1780+ }
1781+ if (upcast(InteractiveGameBase, igamebase, ship->get_owner()->egbase().get_ibase())) {
1782+ if (igamebase->can_act(ship->owner().player_number())) {
1783+ UI::WLMessageBox messagebox(
1784+ get_parent(),
1785+ /** TRANSLATORS: Window label when an expedition can't be canceled */
1786+ _("Cancel Expedition"), _("This expedition can’t be canceled, because the "
1787+ "ship has no port to return to."),
1788+ UI::WLMessageBox::MBoxType::kOk);
1789+ messagebox.run<UI::Panel::Returncodes>();
1790+ }
1791+ }
1792+}
1793+
1794 void ShipWindow::think() {
1795 UI::Window::think();
1796 Widelands::Ship* ship = ship_.get(igbase_.egbase());
1797
1798=== modified file 'src/wui/shipwindow.h'
1799--- src/wui/shipwindow.h 2018-04-17 03:09:29 +0000
1800+++ src/wui/shipwindow.h 2018-04-28 09:26:35 +0000
1801@@ -47,6 +47,7 @@
1802 const std::string& picname,
1803 boost::function<void()> callback);
1804 void set_button_visibility();
1805+ void no_port_error_message();
1806
1807 void act_goto();
1808 void act_destination();
1809@@ -74,7 +75,7 @@
1810 UI::Button* btn_construct_port_;
1811 ItemWaresDisplay* display_;
1812 int navigation_box_height_;
1813- std::unique_ptr<Notifications::Subscriber<Widelands::NoteShipWindow>> shipnotes_subscriber_;
1814+ std::unique_ptr<Notifications::Subscriber<Widelands::NoteShip>> shipnotes_subscriber_;
1815 DISALLOW_COPY_AND_ASSIGN(ShipWindow);
1816 };
1817
1818
1819=== modified file 'src/wui/watchwindow.cc'
1820--- src/wui/watchwindow.cc 2018-04-07 16:59:00 +0000
1821+++ src/wui/watchwindow.cc 2018-04-28 09:26:35 +0000
1822@@ -31,66 +31,20 @@
1823 #include "logic/map_objects/bob.h"
1824 #include "logic/player.h"
1825 #include "profile/profile.h"
1826-#include "ui_basic/button.h"
1827-#include "ui_basic/window.h"
1828 #include "wui/interactive_gamebase.h"
1829 #include "wui/interactive_player.h"
1830-#include "wui/mapview.h"
1831 #include "wui/mapviewpixelconstants.h"
1832 #include "wui/mapviewpixelfunctions.h"
1833
1834-#define NUM_VIEWS 5
1835 #define REFRESH_TIME 5000
1836
1837 // Holds information for a view
1838-struct WatchWindowView {
1839- MapView::View view;
1840- Widelands::ObjectPointer tracking; // if non-null, we're tracking a Bob
1841-};
1842-
1843-struct WatchWindow : public UI::Window {
1844- WatchWindow(InteractiveGameBase& parent,
1845- int32_t x,
1846- int32_t y,
1847- uint32_t w,
1848- uint32_t h,
1849- bool single_window_ = false);
1850- ~WatchWindow() override;
1851-
1852- Widelands::Game& game() const {
1853- return parent_.game();
1854- }
1855-
1856- boost::signals2::signal<void(Vector2f)> warp_mainview;
1857-
1858- void add_view(Widelands::Coords);
1859- void next_view();
1860- void save_coords();
1861- void close_cur_view();
1862- void toggle_buttons();
1863-
1864-protected:
1865- void think() override;
1866- void stop_tracking_by_drag();
1867- void draw(RenderTarget&) override;
1868-
1869-private:
1870- void do_follow();
1871- void do_goto();
1872- void view_button_clicked(uint8_t index);
1873- void set_current_view(uint8_t idx, bool save_previous = true);
1874-
1875- InteractiveGameBase& parent_;
1876- MapView map_view_;
1877- uint32_t last_visit_;
1878- bool single_window_;
1879- uint8_t cur_index_;
1880- UI::Button* view_btns_[NUM_VIEWS];
1881- std::vector<WatchWindowView> views_;
1882-};
1883-
1884 static WatchWindow* g_watch_window = nullptr;
1885
1886+Widelands::Game& WatchWindow::game() const {
1887+ return parent_.game();
1888+}
1889+
1890 WatchWindow::WatchWindow(InteractiveGameBase& parent,
1891 int32_t const x,
1892 int32_t const y,
1893@@ -115,7 +69,7 @@
1894 gotobtn->sigclicked.connect(boost::bind(&WatchWindow::do_goto, this));
1895
1896 if (init_single_window) {
1897- for (uint8_t i = 0; i < NUM_VIEWS; ++i) {
1898+ for (uint8_t i = 0; i < kViews; ++i) {
1899 view_btns_[i] = new UI::Button(this, "view", 74 + (17 * i), 200 - 34, 17, 34,
1900 g_gr->images().get("images/ui_basic/but0.png"), "-");
1901 view_btns_[i]->sigclicked.connect(boost::bind(&WatchWindow::view_button_clicked, this, i));
1902@@ -154,9 +108,9 @@
1903 * This also resets the view cycling timer.
1904 */
1905 void WatchWindow::add_view(Widelands::Coords const coords) {
1906- if (views_.size() >= NUM_VIEWS)
1907+ if (views_.size() >= kViews)
1908 return;
1909- WatchWindowView view;
1910+ WatchWindow::View view;
1911
1912 map_view_.scroll_to_field(coords, MapView::Transition::Jump);
1913
1914@@ -183,7 +137,7 @@
1915
1916 // Enables/Disables buttons for views_
1917 void WatchWindow::toggle_buttons() {
1918- for (uint32_t i = 0; i < NUM_VIEWS; ++i) {
1919+ for (uint32_t i = 0; i < kViews; ++i) {
1920 if (i < views_.size()) {
1921 view_btns_[i]->set_title(std::to_string(i + 1));
1922 view_btns_[i]->set_enabled(true);
1923@@ -256,6 +210,13 @@
1924 }
1925
1926 /**
1927+ * Track the specified bob.
1928+ */
1929+void WatchWindow::follow(Widelands::Bob* bob) {
1930+ views_[cur_index_].tracking = bob;
1931+}
1932+
1933+/**
1934 * Called when the user clicks the "follow" button.
1935 *
1936 * If we are currently tracking a bob, stop tracking.
1937@@ -299,14 +260,14 @@
1938 closest_dist = dist;
1939 }
1940 }
1941- views_[cur_index_].tracking = closest;
1942+ follow(closest);
1943 }
1944 }
1945
1946 /**
1947 * Called when the "go to" button is clicked.
1948 *
1949- * Cause the main map_view_ to jump to our current position.
1950+ * Cause the main mapview_ to jump to our current position.
1951 */
1952 void WatchWindow::do_goto() {
1953 warp_mainview(map_view_.view_area().rect().center());
1954@@ -344,14 +305,16 @@
1955 Open a watch window.
1956 ===============
1957 */
1958-void show_watch_window(InteractiveGameBase& parent, const Widelands::Coords& coords) {
1959+WatchWindow* show_watch_window(InteractiveGameBase& parent, const Widelands::Coords& coords) {
1960 if (g_options.pull_section("global").get_bool("single_watchwin", false)) {
1961 if (!g_watch_window) {
1962 g_watch_window = new WatchWindow(parent, 250, 150, 200, 200, true);
1963 }
1964 g_watch_window->add_view(coords);
1965+ return g_watch_window;
1966 } else {
1967 auto* window = new WatchWindow(parent, 250, 150, 200, 200, false);
1968 window->add_view(coords);
1969+ return window;
1970 }
1971 }
1972
1973=== modified file 'src/wui/watchwindow.h'
1974--- src/wui/watchwindow.h 2016-08-04 15:49:05 +0000
1975+++ src/wui/watchwindow.h 2018-04-28 09:26:35 +0000
1976@@ -22,8 +22,62 @@
1977
1978 #include "logic/widelands_geometry.h"
1979
1980+#include "ui_basic/button.h"
1981+#include "ui_basic/window.h"
1982+#include "wui/mapview.h"
1983+
1984 class InteractiveGameBase;
1985-
1986-void show_watch_window(InteractiveGameBase&, const Widelands::Coords&);
1987+namespace Widelands {
1988+class Game;
1989+}
1990+
1991+struct WatchWindow : public UI::Window {
1992+ WatchWindow(InteractiveGameBase& parent,
1993+ int32_t x,
1994+ int32_t y,
1995+ uint32_t w,
1996+ uint32_t h,
1997+ bool single_window_ = false);
1998+ ~WatchWindow();
1999+
2000+ boost::signals2::signal<void(Vector2f)> warp_mainview;
2001+
2002+ void add_view(Widelands::Coords);
2003+ void follow(Widelands::Bob* bob);
2004+
2005+private:
2006+ static constexpr size_t kViews = 5;
2007+
2008+ // Holds information for a view
2009+ struct View {
2010+ MapView::View view;
2011+ Widelands::ObjectPointer tracking; // if non-null, we're tracking a Bob
2012+ };
2013+
2014+ Widelands::Game& game() const;
2015+
2016+ void think() override;
2017+ void stop_tracking_by_drag();
2018+ void draw(RenderTarget&) override;
2019+ void save_coords();
2020+ void next_view();
2021+ void close_cur_view();
2022+ void toggle_buttons();
2023+
2024+ void do_follow();
2025+ void do_goto();
2026+ void view_button_clicked(uint8_t index);
2027+ void set_current_view(uint8_t idx, bool save_previous = true);
2028+
2029+ InteractiveGameBase& parent_;
2030+ MapView map_view_;
2031+ uint32_t last_visit_;
2032+ bool single_window_;
2033+ uint8_t cur_index_;
2034+ UI::Button* view_btns_[kViews];
2035+ std::vector<WatchWindow::View> views_;
2036+};
2037+
2038+WatchWindow* show_watch_window(InteractiveGameBase&, const Widelands::Coords&);
2039
2040 #endif // end of include guard: WL_WUI_WATCHWINDOW_H

Subscribers

People subscribed via source and target branches

to status/vote changes: