Merge lp:~widelands-dev/widelands/multiplayer_dropdowns into lp:widelands

Proposed by GunChleoc on 2017-06-26
Status: Merged
Merged at revision: 8445
Proposed branch: lp:~widelands-dev/widelands/multiplayer_dropdowns
Merge into: lp:widelands
Prerequisite: lp:~widelands-dev/widelands/multiplayer_dropdowns_2_init_team
Diff against target: 2785 lines (+1002/-778)
29 files modified
src/ai/computer_player.cc (+7/-6)
src/ai/computer_player.h (+17/-0)
src/ai/defaultai.h (+21/-18)
src/graphic/playercolor.h (+13/-0)
src/logic/CMakeLists.txt (+1/-0)
src/logic/game.cc (+3/-3)
src/logic/game_settings.cc (+38/-1)
src/logic/game_settings.h (+47/-3)
src/logic/single_player_game_settings_provider.cc (+9/-8)
src/logic/single_player_game_settings_provider.h (+1/-1)
src/network/CMakeLists.txt (+0/-1)
src/network/gameclient.cc (+11/-6)
src/network/gameclient.h (+2/-2)
src/network/gamehost.cc (+76/-113)
src/network/gamehost.h (+1/-1)
src/network/network_player_settings_backend.cc (+58/-123)
src/network/network_player_settings_backend.h (+9/-20)
src/network/network_protocol.h (+3/-2)
src/notifications/note_ids.h (+3/-1)
src/ui_basic/dropdown.cc (+67/-12)
src/ui_basic/dropdown.h (+31/-2)
src/ui_basic/listselect.cc (+2/-2)
src/ui_fsmenu/launch_game.cc (+5/-6)
src/ui_fsmenu/launch_mpg.cc (+30/-20)
src/ui_fsmenu/launch_mpg.h (+1/-0)
src/ui_fsmenu/launch_spg.cc (+5/-5)
src/wui/multiplayersetupgroup.cc (+528/-409)
src/wui/multiplayersetupgroup.h (+6/-6)
src/wui/playerdescrgroup.cc (+7/-7)
To merge this branch: bzr merge lp:~widelands-dev/widelands/multiplayer_dropdowns
Reviewer Review Type Date Requested Status
SirVer testing,compile,code-review Approve on 2017-08-31
kaputtnik testing 2017-06-26 Approve on 2017-06-27
Review via email: mp+326302@code.launchpad.net

Commit Message

For multiplayer game setup, use dropdowns instead of buttons to select player states (open, closed, human, AIs, shared in). This uses 2 new notifications:
- NoteGameSettings to synchronize and update the game setup dropdowns
- NoteDropdown to ensure that only 1 dropdown is ever open at the same time

Also:

- Dropdown lists will now automatically expand upwards rather than downwards if they wouldn't fit on screen.

- Removed the tiny labels in the multiplayer game setup, since the function of each dropdown can be identified per its tooltip

- Pulled out some functions to avoid code duplications and shifted some calculations to GameHost.

Description of the Change

This is the final branch in a 3-branch series to change all buttons in multiplayer setup to dropdown menus.

All BUGFIXING and TESTING is to be done ON THIS BRANCH.

I split this into 3 branches to try to make the code review diffs more manageable.

The interaction is complex, so this will need thorough testing. I tried to click on all possible combinations, but I might have missed some.

1. The dropdowns should dynamically change their contents to only contain usable entries
2. Server / client should always be in sync (apart from delays for processing everything)
3. No missing information
4. No crashes

To post a comment you must log in.
8424. By GunChleoc on 2017-06-26

Fixed some comments.

8425. By GunChleoc on 2017-06-26

UI::Textarea name; is no longer a pointer.

8426. By GunChleoc on 2017-06-26

Removed superfluous arguments from MultiPlayerPlayerGroup constructor.

8427. By GunChleoc on 2017-06-26

Resolved a naming conflict.

kaputtnik (franku) wrote :

Does sometimes work, sometimes not (used the LAN/Direct IP setup):

Mostly the drop down box appear for a very short time after the mouse button is released.
Sometimes the drop down box appears when releasing the mouse button.
When the drop down box appears for one button, most of the other buttons shows the drop down box.
The described behave is unspecific to a particular button. E.g. sometimes the Tribe selection works, sometimes not.

Used the 'Nile' map for testing.

review: Needs Fixing
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 2381. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/247251733.
Appveyor build 2209. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_multiplayer_dropdowns-2209.

GunChleoc (gunchleoc) wrote :

I have now tried this with setting up games between my Linux and Windows machines using the AppVeyor build, and I get none of these problems. Not a single one, all dropdowns work perfectly. So, I have no clue how I could possibly debug this :(

kaputtnik (franku) wrote :

I found what causing this:

Please try to click on the top area of a drop down button, so the tooltip is very near (or overlaps) the button. Then the drop down box disappear after mouse release.

If i click in the bottom area, all works fine.

8428. By GunChleoc on 2017-06-27

Increase mouse tolerance on the top edge of dropdowns.

GunChleoc (gunchleoc) wrote :

Now I get what you mean. Dropdowns will autoclose when you move the mouse away, and the tolerance on the top edge was too small. Should be fixed now.

kaputtnik (franku) wrote :

Yes, it's working now :-)

I am not familiar with the mutliplayer things, so i guess it's ok that the mutliplayer scenarios shows "Suggested teams" but one could not setup a team (e.g. Smugglers). Nevertheless disturbing.
This applies also to the map "Trident of fire" which is not a multiplayer scenario.

As said, i think this is ok, just wanted to mention here.

From what i can see all other things are fine:-)

review: Approve (testing)
GunChleoc (gunchleoc) wrote :

Great that it was an easy fix for once :)

Regarding the suggested teams, that's a feature that I've had in my head for years now. Once we have finished overhauling all the screens, I will get to work. Also, once the overhaul is finished, I will make some options for the win conditions configurable, like length of game and fog of war. This will all have to wait for Build 21 though.

kaputtnik (franku) wrote :

Thanks for implementing this. I think this is a great enhancement to the UI :-)

8429. By GunChleoc on 2017-06-28

Merged trunk.

8430. By GunChleoc on 2017-08-11

Merged trunk.

8431. By GunChleoc on 2017-08-18

Merged trunk.

8432. By GunChleoc on 2017-08-30

Merged trunk.

SirVer (sirver) wrote :

Code lgtm. That was a lot of work, thanks for doing this!

I tested it and have a few comments:
- The team UI is rather clever - maybe too clever, we'll see what the users say.
- I found one issue: If I change myself to be dual-control with another player, I can change their team and tribe (expected). If I now change away again from dual-control, I can still change their team and tribe in my UI (but others are not updated, so I think only the button needs to be disabled)

Please see the other code reviews for the base branches for some more comments.

Great feature! Thanks.

review: Approve (testing,compile,code-review)
8433. By GunChleoc on 2017-09-02

Replaced cast_signed/unsigned macros with static_cast.

8434. By GunChleoc on 2017-09-02

Merged trunk.

GunChleoc (gunchleoc) wrote :

Thanks for the code review and the testing!

I have kicked out the new cast_(un)signed macros and have created 2 bug reports for the remaining issues.

@bunnybot merge

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'data/images/players/no_team.png'
2Binary files data/images/players/no_team.png 1970-01-01 00:00:00 +0000 and data/images/players/no_team.png 2017-09-02 13:55:49 +0000 differ
3=== added file 'data/images/players/team.png'
4Binary files data/images/players/team.png 1970-01-01 00:00:00 +0000 and data/images/players/team.png 2017-09-02 13:55:49 +0000 differ
5=== added file 'data/images/players/team_pc.png'
6Binary files data/images/players/team_pc.png 1970-01-01 00:00:00 +0000 and data/images/players/team_pc.png 2017-09-02 13:55:49 +0000 differ
7=== modified file 'src/ai/computer_player.cc'
8--- src/ai/computer_player.cc 2017-01-25 18:55:59 +0000
9+++ src/ai/computer_player.cc 2017-09-02 13:55:49 +0000
10@@ -36,12 +36,13 @@
11 }
12
13 struct EmptyAIImpl : Implementation {
14- EmptyAIImpl() {
15- name = "empty";
16- /** TRANSLATORS: This is the name of an AI used in the game setup screens */
17- descname = _("No AI");
18- icon_filename = "images/ai/ai_empty.png";
19- type = Implementation::Type::kEmpty;
20+ EmptyAIImpl()
21+ : Implementation(
22+ "empty",
23+ /** TRANSLATORS: This is the name of an AI used in the game setup screens */
24+ _("No AI"),
25+ "images/ai/ai_empty.png",
26+ Implementation::Type::kEmpty) {
27 }
28 ComputerPlayer* instantiate(Widelands::Game& g,
29 Widelands::PlayerNumber const pid) const override {
30
31=== modified file 'src/ai/computer_player.h'
32--- src/ai/computer_player.h 2017-04-22 08:02:21 +0000
33+++ src/ai/computer_player.h 2017-09-02 13:55:49 +0000
34@@ -23,9 +23,15 @@
35 #include <string>
36 #include <vector>
37
38+#include <boost/algorithm/string/predicate.hpp>
39+
40 #include "base/macros.h"
41 #include "logic/widelands.h"
42
43+// We need to use a string prefix in the game setup screens to identify the AIs, so we make sure
44+// that the AI names don't contain the separator that's used to parse the strings there.
45+#define AI_NAME_SEPARATOR "|"
46+
47 namespace Widelands {
48 class Game;
49 } // namespace Widelands
50@@ -61,6 +67,17 @@
51 std::string descname;
52 std::string icon_filename;
53 Type type;
54+ explicit Implementation(std::string init_name,
55+ std::string init_descname,
56+ std::string init_icon_filename,
57+ Type init_type)
58+ : name(init_name),
59+ descname(init_descname),
60+ icon_filename(init_icon_filename),
61+ type(init_type) {
62+ assert(!boost::contains(name, AI_NAME_SEPARATOR));
63+ }
64+
65 virtual ~Implementation() {
66 }
67 virtual ComputerPlayer* instantiate(Widelands::Game&, Widelands::PlayerNumber) const = 0;
68
69=== modified file 'src/ai/defaultai.h'
70--- src/ai/defaultai.h 2017-08-14 18:31:43 +0000
71+++ src/ai/defaultai.h 2017-09-02 13:55:49 +0000
72@@ -91,12 +91,13 @@
73
74 /// Implementation for Strong
75 struct NormalImpl : public ComputerPlayer::Implementation {
76- NormalImpl() {
77- name = "normal";
78- /** TRANSLATORS: This is the name of an AI used in the game setup screens */
79- descname = _("Normal AI");
80- icon_filename = "images/ai/ai_normal.png";
81- type = Implementation::Type::kDefault;
82+ NormalImpl()
83+ : Implementation(
84+ "normal",
85+ /** TRANSLATORS: This is the name of an AI used in the game setup screens */
86+ _("Normal AI"),
87+ "images/ai/ai_normal.png",
88+ Implementation::Type::kDefault) {
89 }
90 ComputerPlayer* instantiate(Widelands::Game& game,
91 Widelands::PlayerNumber const p) const override {
92@@ -105,12 +106,13 @@
93 };
94
95 struct WeakImpl : public ComputerPlayer::Implementation {
96- WeakImpl() {
97- name = "weak";
98- /** TRANSLATORS: This is the name of an AI used in the game setup screens */
99- descname = _("Weak AI");
100- icon_filename = "images/ai/ai_weak.png";
101- type = Implementation::Type::kDefault;
102+ WeakImpl()
103+ : Implementation(
104+ "weak",
105+ /** TRANSLATORS: This is the name of an AI used in the game setup screens */
106+ _("Weak AI"),
107+ "images/ai/ai_weak.png",
108+ Implementation::Type::kDefault) {
109 }
110 ComputerPlayer* instantiate(Widelands::Game& game,
111 Widelands::PlayerNumber const p) const override {
112@@ -119,12 +121,13 @@
113 };
114
115 struct VeryWeakImpl : public ComputerPlayer::Implementation {
116- VeryWeakImpl() {
117- name = "very_weak";
118- /** TRANSLATORS: This is the name of an AI used in the game setup screens */
119- descname = _("Very Weak AI");
120- icon_filename = "images/ai/ai_very_weak.png";
121- type = Implementation::Type::kDefault;
122+ VeryWeakImpl()
123+ : Implementation(
124+ "very_weak",
125+ /** TRANSLATORS: This is the name of an AI used in the game setup screens */
126+ _("Very Weak AI"),
127+ "images/ai/ai_very_weak.png",
128+ Implementation::Type::kDefault) {
129 }
130 ComputerPlayer* instantiate(Widelands::Game& game,
131 Widelands::PlayerNumber const p) const override {
132
133=== modified file 'src/graphic/playercolor.h'
134--- src/graphic/playercolor.h 2017-04-28 06:47:01 +0000
135+++ src/graphic/playercolor.h 2017-09-02 13:55:49 +0000
136@@ -49,6 +49,19 @@
137 RGBColor(144, 144, 144), // light gray
138 };
139
140+// Hard coded team colors
141+const RGBColor kTeamColors[kMaxPlayers / 2 + 1] = {
142+ RGBColor(100, 100, 100), // No team
143+ RGBColor(2, 2, 198), // blue
144+ RGBColor(255, 41, 0), // red
145+ RGBColor(255, 232, 0), // yellow
146+ RGBColor(59, 223, 3), // green
147+ RGBColor(57, 57, 57), // black/dark gray
148+ RGBColor(255, 172, 0), // orange
149+ RGBColor(215, 0, 218), // purple
150+ RGBColor(255, 255, 255), // white
151+};
152+
153 /// Looks for a player color mask image, and if it finds one,
154 /// returns the image with added playercolor. If no player color
155 /// image file is found, gets the image from 'image_filename'
156
157=== modified file 'src/logic/CMakeLists.txt'
158--- src/logic/CMakeLists.txt 2017-07-11 19:38:04 +0000
159+++ src/logic/CMakeLists.txt 2017-09-02 13:55:49 +0000
160@@ -13,6 +13,7 @@
161 io_filesystem
162 logic
163 logic_constants
164+ notifications
165 scripting_lua_interface
166 scripting_lua_table
167 )
168
169=== modified file 'src/logic/game.cc'
170--- src/logic/game.cc 2017-08-20 06:43:11 +0000
171+++ src/logic/game.cc 2017-09-02 13:55:49 +0000
172@@ -269,10 +269,10 @@
173 for (uint32_t i = 0; i < settings.players.size(); ++i) {
174 const PlayerSettings& playersettings = settings.players[i];
175
176- if (playersettings.state == PlayerSettings::stateClosed ||
177- playersettings.state == PlayerSettings::stateOpen)
178+ if (playersettings.state == PlayerSettings::State::kClosed ||
179+ playersettings.state == PlayerSettings::State::kOpen)
180 continue;
181- else if (playersettings.state == PlayerSettings::stateShared) {
182+ else if (playersettings.state == PlayerSettings::State::kShared) {
183 shared.push_back(playersettings);
184 shared_num.push_back(i + 1);
185 continue;
186
187=== modified file 'src/logic/game_settings.cc'
188--- src/logic/game_settings.cc 2017-06-05 07:48:28 +0000
189+++ src/logic/game_settings.cc 2017-09-02 13:55:49 +0000
190@@ -1,1 +1,38 @@
191-// Dummy to make CMake happy
192+/*
193+ * Copyright (C) 2017 by the Widelands Development Team
194+ *
195+ * This program is free software; you can redistribute it and/or
196+ * modify it under the terms of the GNU General Public License
197+ * as published by the Free Software Foundation; either version 2
198+ * of the License, or (at your option) any later version.
199+ *
200+ * This program is distributed in the hope that it will be useful,
201+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
202+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
203+ * GNU General Public License for more details.
204+ *
205+ * You should have received a copy of the GNU General Public License
206+ * along with this program; if not, write to the Free Software
207+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
208+ *
209+ */
210+
211+#include "logic/game_settings.h"
212+
213+Widelands::PlayerNumber GameSettings::find_shared(PlayerSlot slot) const {
214+ Widelands::PlayerNumber result = 1;
215+ for (; result <= players.size(); ++result) {
216+ if (PlayerSettings::can_be_shared(players.at(result - 1).state) && (result - 1) != slot) {
217+ break;
218+ }
219+ }
220+ return result;
221+}
222+
223+bool GameSettings::is_shared_usable(PlayerSlot slot, Widelands::PlayerNumber shared) const {
224+ return shared <= players.size() && (shared - 1) != slot;
225+}
226+
227+bool GameSettings::uncloseable(PlayerSlot slot) const {
228+ return (scenario && !players.at(slot).closeable) || savegame;
229+}
230
231=== modified file 'src/logic/game_settings.h'
232--- src/logic/game_settings.h 2017-08-16 04:31:56 +0000
233+++ src/logic/game_settings.h 2017-09-02 13:55:49 +0000
234@@ -28,11 +28,22 @@
235 #include "logic/map_objects/tribes/tribe_basic_info.h"
236 #include "logic/player_end_result.h"
237 #include "logic/widelands.h"
238+#include "notifications/note_ids.h"
239+#include "notifications/notifications.h"
240 #include "scripting/lua_interface.h"
241 #include "scripting/lua_table.h"
242
243+// PlayerSlot 0 will give us Widelands::PlayerNumber 1 etc., so we rename it to avoid confusion.
244+// TODO(GunChleoc): Rename all uint8_t to PlayerSlot or Widelands::PlayerNumber
245+using PlayerSlot = Widelands::PlayerNumber;
246+
247 struct PlayerSettings {
248- enum State { stateOpen, stateHuman, stateComputer, stateClosed, stateShared };
249+ enum class State { kOpen, kHuman, kComputer, kClosed, kShared };
250+
251+ /// Returns whether the given state allows sharing a slot at all
252+ static bool can_be_shared(PlayerSettings::State state) {
253+ return state != PlayerSettings::State::kClosed && state != PlayerSettings::State::kShared;
254+ }
255
256 State state;
257 uint8_t initialization_index;
258@@ -71,6 +82,28 @@
259 // not
260 };
261
262+/// The gamehost/gameclient are sending those to notify about status changes, which are then picked
263+/// up by the UI.
264+struct NoteGameSettings {
265+ CAN_BE_SENT_AS_NOTE(NoteId::GameSettings)
266+
267+ enum class Action {
268+ kUser, // A client has picked a different player slot / become an observer
269+ kPlayer, // A player slot has changed its status (type, tribe etc.)
270+ kMap // A new map/savegame was selected
271+ };
272+
273+ Action action;
274+ PlayerSlot position;
275+ uint8_t usernum;
276+
277+ explicit NoteGameSettings(Action init_action,
278+ PlayerSlot init_position = std::numeric_limits<uint8_t>::max(),
279+ uint8_t init_usernum = UserSettings::none())
280+ : action(init_action), position(init_position), usernum(init_usernum) {
281+ }
282+};
283+
284 /**
285 * Holds all settings about a game that can be configured before the
286 * game actually starts.
287@@ -92,6 +125,14 @@
288 }
289 }
290
291+ /// Find a player number that the slot could share in. Does not guarantee that a viable slot was
292+ /// actually found.
293+ Widelands::PlayerNumber find_shared(PlayerSlot slot) const;
294+ /// Check if the player number returned by find_shared is usable
295+ bool is_shared_usable(PlayerSlot slot, Widelands::PlayerNumber shared) const;
296+ /// Savegame slots and certain scenario slots can't be closed
297+ bool uncloseable(PlayerSlot slot) const;
298+
299 /// Number of player position
300 int16_t playernum;
301 /// Number of users entry
302@@ -154,7 +195,9 @@
303 bool savegame = false) = 0;
304 virtual void set_player_state(uint8_t number, PlayerSettings::State) = 0;
305 virtual void set_player_ai(uint8_t number, const std::string&, bool const random_ai = false) = 0;
306- virtual void next_player_state(uint8_t number) = 0;
307+ // Multiplayer no longer toggles per button
308+ virtual void next_player_state(uint8_t /* number */) {
309+ }
310 virtual void
311 set_player_tribe(uint8_t number, const std::string&, bool const random_tribe = false) = 0;
312 virtual void set_player_init(uint8_t number, uint8_t index) = 0;
313@@ -163,10 +206,11 @@
314 virtual void set_player_number(uint8_t number) = 0;
315 virtual void set_player_team(uint8_t number, Widelands::TeamNumber team) = 0;
316 virtual void set_player_closeable(uint8_t number, bool closeable) = 0;
317- virtual void set_player_shared(uint8_t number, uint8_t shared) = 0;
318+ virtual void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) = 0;
319 virtual void set_win_condition_script(const std::string& wc) = 0;
320 virtual std::string get_win_condition_script() = 0;
321
322+ // For retrieving tips texts
323 struct NoTribe {};
324 const std::string& get_players_tribe() {
325 if (UserSettings::highest_playernum() < settings().playernum)
326
327=== modified file 'src/logic/single_player_game_settings_provider.cc'
328--- src/logic/single_player_game_settings_provider.cc 2017-08-09 19:25:04 +0000
329+++ src/logic/single_player_game_settings_provider.cc 2017-09-02 13:55:49 +0000
330@@ -81,14 +81,15 @@
331
332 while (oldplayers < maxplayers) {
333 PlayerSettings& player = s.players[oldplayers];
334- player.state = (oldplayers == 0) ? PlayerSettings::stateHuman : PlayerSettings::stateComputer;
335+ player.state =
336+ (oldplayers == 0) ? PlayerSettings::State::kHuman : PlayerSettings::State::kComputer;
337 player.tribe = s.tribes.at(0).name;
338 player.random_tribe = false;
339 player.initialization_index = 0;
340 player.name = (boost::format(_("Player %u")) % (oldplayers + 1)).str();
341 player.team = 0;
342 // Set default computerplayer ai type
343- if (player.state == PlayerSettings::stateComputer) {
344+ if (player.state == PlayerSettings::State::kComputer) {
345 const ComputerPlayer::ImplementationVector& impls = ComputerPlayer::get_implementations();
346 if (impls.size() > 1) {
347 player.ai = impls.at(0)->name;
348@@ -107,8 +108,8 @@
349 if (number == s.playernum || number >= s.players.size())
350 return;
351
352- if (state == PlayerSettings::stateOpen)
353- state = PlayerSettings::stateComputer;
354+ if (state == PlayerSettings::State::kOpen)
355+ state = PlayerSettings::State::kComputer;
356
357 s.players[number].state = state;
358 }
359@@ -147,7 +148,7 @@
360 s.players[number].ai = (*it)->name;
361 }
362
363- s.players[number].state = PlayerSettings::stateComputer;
364+ s.players[number].state = PlayerSettings::State::kComputer;
365 }
366
367 void SinglePlayerGameSettingsProvider::set_player_tribe(uint8_t const number,
368@@ -199,7 +200,7 @@
369 // nothing to do
370 }
371
372-void SinglePlayerGameSettingsProvider::set_player_shared(uint8_t, uint8_t) {
373+void SinglePlayerGameSettingsProvider::set_player_shared(PlayerSlot, Widelands::PlayerNumber) {
374 // nothing to do
375 }
376
377@@ -219,8 +220,8 @@
378 return;
379 PlayerSettings const position = settings().players.at(number);
380 PlayerSettings const player = settings().players.at(settings().playernum);
381- if (number < settings().players.size() && (position.state == PlayerSettings::stateOpen ||
382- position.state == PlayerSettings::stateComputer)) {
383+ if (number < settings().players.size() && (position.state == PlayerSettings::State::kOpen ||
384+ position.state == PlayerSettings::State::kComputer)) {
385 set_player(number, player);
386 set_player(settings().playernum, position);
387 s.playernum = number;
388
389=== modified file 'src/logic/single_player_game_settings_provider.h'
390--- src/logic/single_player_game_settings_provider.h 2017-02-10 14:12:36 +0000
391+++ src/logic/single_player_game_settings_provider.h 2017-09-02 13:55:49 +0000
392@@ -54,7 +54,7 @@
393 void set_player_init(uint8_t const number, uint8_t const index) override;
394 void set_player_team(uint8_t number, Widelands::TeamNumber team) override;
395 void set_player_closeable(uint8_t, bool) override;
396- void set_player_shared(uint8_t, uint8_t) override;
397+ void set_player_shared(PlayerSlot, Widelands::PlayerNumber) override;
398 void set_player_name(uint8_t const number, const std::string& name) override;
399 void set_player(uint8_t const number, const PlayerSettings& ps) override;
400 void set_player_number(uint8_t const number) override;
401
402=== modified file 'src/network/CMakeLists.txt'
403--- src/network/CMakeLists.txt 2017-05-14 21:06:23 +0000
404+++ src/network/CMakeLists.txt 2017-09-02 13:55:49 +0000
405@@ -33,7 +33,6 @@
406 build_info
407 chat
408 game_io
409- graphic_playercolor
410 helper
411 io_fileread
412 io_filesystem
413
414=== modified file 'src/network/gameclient.cc'
415--- src/network/gameclient.cc 2017-08-09 17:55:34 +0000
416+++ src/network/gameclient.cc 2017-09-02 13:55:49 +0000
417@@ -356,25 +356,25 @@
418 // client is not allowed to do this
419 }
420
421-void GameClient::set_player_shared(uint8_t number, uint8_t player) {
422+void GameClient::set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) {
423 if ((number != d->settings.playernum))
424 return;
425
426 SendPacket s;
427 s.unsigned_8(NETCMD_SETTING_CHANGESHARED);
428 s.unsigned_8(number);
429- s.unsigned_8(player);
430+ s.unsigned_8(shared);
431 d->net->send(s);
432 }
433
434-void GameClient::set_player_init(uint8_t number, uint8_t) {
435+void GameClient::set_player_init(uint8_t number, uint8_t initialization_index) {
436 if ((number != d->settings.playernum))
437 return;
438
439- // Host will decide what to change, therefore the init is not send, just the request to change
440 SendPacket s;
441 s.unsigned_8(NETCMD_SETTING_CHANGEINIT);
442 s.unsigned_8(number);
443+ s.unsigned_8(initialization_index);
444 d->net->send(s);
445 }
446
447@@ -404,8 +404,8 @@
448 return;
449 // Same if the player is not selectable
450 if (number < d->settings.players.size() &&
451- (d->settings.players.at(number).state == PlayerSettings::stateClosed ||
452- d->settings.players.at(number).state == PlayerSettings::stateComputer))
453+ (d->settings.players.at(number).state == PlayerSettings::State::kClosed ||
454+ d->settings.players.at(number).state == PlayerSettings::State::kComputer))
455 return;
456
457 // Send request
458@@ -459,6 +459,7 @@
459 player.random_ai = packet.unsigned_8() == 1;
460 player.team = packet.unsigned_8();
461 player.shared_in = packet.unsigned_8();
462+ Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kPlayer, number));
463 }
464
465 void GameClient::receive_one_user(uint32_t const number, StreamRead& packet) {
466@@ -473,10 +474,13 @@
467 d->settings.users.at(number).name = packet.string();
468 d->settings.users.at(number).position = packet.signed_32();
469 d->settings.users.at(number).ready = packet.unsigned_8() == 1;
470+
471 if (static_cast<int32_t>(number) == d->settings.usernum) {
472 d->localplayername = d->settings.users.at(number).name;
473 d->settings.playernum = d->settings.users.at(number).position;
474 }
475+ Notifications::publish(
476+ NoteGameSettings(NoteGameSettings::Action::kUser, d->settings.playernum, number));
477 }
478
479 void GameClient::send(const std::string& msg) {
480@@ -567,6 +571,7 @@
481 // New map was set, so we clean up the buffer of a previously requested file
482 if (file_)
483 delete file_;
484+ Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kMap));
485 break;
486 }
487
488
489=== modified file 'src/network/gameclient.h'
490--- src/network/gameclient.h 2017-07-05 19:21:57 +0000
491+++ src/network/gameclient.h 2017-09-02 13:55:49 +0000
492@@ -84,13 +84,13 @@
493 virtual void set_player_tribe(uint8_t number,
494 const std::string& tribe,
495 bool const random_tribe = false) override;
496- void set_player_init(uint8_t number, uint8_t index) override;
497+ void set_player_init(uint8_t number, uint8_t initialization_index) override;
498 void set_player_name(uint8_t number, const std::string& name) override;
499 void set_player(uint8_t number, const PlayerSettings& ps) override;
500 void set_player_number(uint8_t number) override;
501 void set_player_team(uint8_t number, Widelands::TeamNumber team) override;
502 void set_player_closeable(uint8_t number, bool closeable) override;
503- void set_player_shared(uint8_t number, uint8_t shared) override;
504+ void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) override;
505 void set_win_condition_script(const std::string&) override;
506 std::string get_win_condition_script() override;
507
508
509=== modified file 'src/network/gamehost.cc'
510--- src/network/gamehost.cc 2017-08-19 23:24:28 +0000
511+++ src/network/gamehost.cc 2017-09-02 13:55:49 +0000
512@@ -81,13 +81,16 @@
513 return true;
514 }
515 bool can_change_player_state(uint8_t const number) override {
516+ if (number >= settings().players.size()) {
517+ return false;
518+ }
519 if (settings().savegame)
520- return settings().players.at(number).state != PlayerSettings::stateClosed;
521+ return settings().players.at(number).state != PlayerSettings::State::kClosed;
522 else if (settings().scenario)
523- return ((settings().players.at(number).state == PlayerSettings::stateOpen ||
524- settings().players.at(number).state == PlayerSettings::stateHuman) &&
525+ return ((settings().players.at(number).state == PlayerSettings::State::kOpen ||
526+ settings().players.at(number).state == PlayerSettings::State::kHuman) &&
527 settings().players.at(number).closeable) ||
528- settings().players.at(number).state == PlayerSettings::stateClosed;
529+ settings().players.at(number).state == PlayerSettings::State::kClosed;
530 return true;
531 }
532 bool can_change_player_tribe(uint8_t const number) override {
533@@ -105,7 +108,7 @@
534 return false;
535 if (number == settings().playernum)
536 return true;
537- return settings().players.at(number).state == PlayerSettings::stateComputer;
538+ return settings().players.at(number).state == PlayerSettings::State::kComputer;
539 }
540
541 bool can_launch() override {
542@@ -124,83 +127,6 @@
543
544 host_->set_player_state(number, state);
545 }
546- void next_player_state(uint8_t const number) override {
547- if (number > settings().players.size())
548- return;
549-
550- PlayerSettings::State newstate = PlayerSettings::stateClosed;
551- switch (host_->settings().players.at(number).state) {
552- case PlayerSettings::stateClosed:
553- // In savegames : closed players can not be changed.
554- assert(!host_->settings().savegame);
555- newstate = PlayerSettings::stateOpen;
556- break;
557- case PlayerSettings::stateOpen:
558- case PlayerSettings::stateHuman:
559- if (host_->settings().scenario) {
560- assert(host_->settings().players.at(number).closeable);
561- newstate = PlayerSettings::stateClosed;
562- break;
563- } // else fall through
564- FALLS_THROUGH;
565- case PlayerSettings::stateComputer: {
566- const ComputerPlayer::ImplementationVector& impls = ComputerPlayer::get_implementations();
567- ComputerPlayer::ImplementationVector::const_iterator it = impls.begin();
568- if (host_->settings().players.at(number).ai.empty()) {
569- set_player_ai(number, (*it)->name);
570- newstate = PlayerSettings::stateComputer;
571- break;
572- }
573- do {
574- ++it;
575- if ((*(it - 1))->name == host_->settings().players.at(number).ai)
576- break;
577- } while (it != impls.end());
578- if (settings().players.at(number).random_ai) {
579- set_player_ai(number, std::string());
580- set_player_name(number, std::string());
581- // Do not share a player in savegames or scenarios
582- if (host_->settings().scenario || host_->settings().savegame)
583- newstate = PlayerSettings::stateOpen;
584- else {
585- uint8_t shared = 0;
586- for (; shared < settings().players.size(); ++shared) {
587- if (settings().players.at(shared).state != PlayerSettings::stateClosed &&
588- settings().players.at(shared).state != PlayerSettings::stateShared)
589- break;
590- }
591- if (shared < settings().players.size()) {
592- newstate = PlayerSettings::stateShared;
593- set_player_shared(number, shared + 1);
594- } else
595- newstate = PlayerSettings::stateClosed;
596- }
597- } else if (it == impls.end()) {
598- do {
599- uint8_t random = (std::rand() % impls.size()); // Choose a random AI
600- it = impls.begin() + random;
601- } while ((*it)->type == ComputerPlayer::Implementation::Type::kEmpty);
602- set_player_ai(number, (*it)->name, true);
603- newstate = PlayerSettings::stateComputer;
604- break;
605- } else {
606- set_player_ai(number, (*it)->name);
607- newstate = PlayerSettings::stateComputer;
608- }
609- break;
610- }
611- case PlayerSettings::stateShared: {
612- // Do not close a player in savegames or scenarios
613- if (host_->settings().scenario || host_->settings().savegame)
614- newstate = PlayerSettings::stateOpen;
615- else
616- newstate = PlayerSettings::stateClosed;
617- break;
618- }
619- }
620-
621- host_->set_player_state(number, newstate, true);
622- }
623
624 void
625 set_player_tribe(uint8_t number, const std::string& tribe, bool const random_tribe) override {
626@@ -208,9 +134,10 @@
627 return;
628
629 if (number == settings().playernum ||
630- settings().players.at(number).state == PlayerSettings::stateComputer ||
631- settings().players.at(number).state == PlayerSettings::stateShared ||
632- settings().players.at(number).state == PlayerSettings::stateOpen) // For savegame loading
633+ settings().players.at(number).state == PlayerSettings::State::kComputer ||
634+ settings().players.at(number).state == PlayerSettings::State::kShared ||
635+ settings().players.at(number).state ==
636+ PlayerSettings::State::kOpen) // For savegame loading
637 host_->set_player_tribe(number, tribe, random_tribe);
638 }
639
640@@ -219,7 +146,7 @@
641 return;
642
643 if (number == settings().playernum ||
644- settings().players.at(number).state == PlayerSettings::stateComputer)
645+ settings().players.at(number).state == PlayerSettings::State::kComputer)
646 host_->set_player_team(number, team);
647 }
648
649@@ -229,7 +156,7 @@
650 host_->set_player_closeable(number, closeable);
651 }
652
653- void set_player_shared(uint8_t number, uint8_t shared) override {
654+ void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) override {
655 if (number >= host_->settings().players.size())
656 return;
657 host_->set_player_shared(number, shared);
658@@ -1019,9 +946,9 @@
659 // but not all should be closed!
660 bool one_not_closed = false;
661 for (PlayerSettings& setting : d->settings.players) {
662- if (setting.state != PlayerSettings::stateClosed)
663+ if (setting.state != PlayerSettings::State::kClosed)
664 one_not_closed = true;
665- if (setting.state == PlayerSettings::stateOpen)
666+ if (setting.state == PlayerSettings::State::kOpen)
667 return false;
668 }
669 return one_not_closed;
670@@ -1071,7 +998,7 @@
671
672 while (oldplayers < maxplayers) {
673 PlayerSettings& player = d->settings.players.at(oldplayers);
674- player.state = PlayerSettings::stateOpen;
675+ player.state = PlayerSettings::State::kOpen;
676 player.name = "";
677 player.tribe = d->settings.tribes.at(0).name;
678 player.random_tribe = false;
679@@ -1146,9 +1073,7 @@
680 if (player.state == state)
681 return;
682
683- SendPacket s;
684-
685- if (player.state == PlayerSettings::stateHuman) {
686+ if (player.state == PlayerSettings::State::kHuman) {
687 // 0 is host and has no client
688 if (d->settings.users.at(0).position == number) {
689 d->settings.users.at(0).position = UserSettings::none();
690@@ -1168,13 +1093,6 @@
691 break;
692 }
693 }
694-
695- // broadcast change
696- s.unsigned_8(NETCMD_SETTING_USER);
697- s.unsigned_32(i);
698- write_setting_user(s, i);
699- broadcast(s);
700-
701 break;
702 }
703 }
704@@ -1182,15 +1100,53 @@
705
706 player.state = state;
707
708- if (player.state == PlayerSettings::stateComputer)
709+ // Make sure that shared slots have a player number to share in
710+ if (player.state == PlayerSettings::State::kShared) {
711+ const PlayerSlot shared = d->settings.find_shared(number);
712+ if (d->settings.is_shared_usable(number, shared)) {
713+ set_player_shared(number, shared);
714+ } else {
715+ player.state = PlayerSettings::State::kClosed;
716+ }
717+ }
718+
719+ // Update shared positions for other players
720+ for (size_t i = 0; i < d->settings.players.size(); ++i) {
721+ if (i == number) {
722+ // Don't set own state
723+ continue;
724+ }
725+ if (d->settings.players.at(i).state == PlayerSettings::State::kShared) {
726+ const PlayerSlot shared = d->settings.find_shared(i);
727+ if (d->settings.is_shared_usable(i, shared)) {
728+ set_player_shared(i, shared);
729+ } else {
730+ set_player_state(i, PlayerSettings::State::kClosed, host);
731+ }
732+ }
733+ }
734+
735+ // Make sure that slots that are not closeable stay open
736+ if (player.state == PlayerSettings::State::kClosed && d->settings.uncloseable(number)) {
737+ player.state = PlayerSettings::State::kOpen;
738+ }
739+
740+ if (player.state == PlayerSettings::State::kComputer)
741 player.name = get_computer_player_name(number);
742
743- // Broadcast change
744+ // Broadcast change to player
745+ SendPacket s;
746 s.reset();
747 s.unsigned_8(NETCMD_SETTING_PLAYER);
748 s.unsigned_8(number);
749 write_setting_player(s, number);
750 broadcast(s);
751+
752+ // Let clients know whether their slot has changed
753+ s.reset();
754+ s.unsigned_8(NETCMD_SETTING_ALLUSERS);
755+ write_setting_all_users(s);
756+ broadcast(s);
757 }
758
759 void GameHost::set_player_tribe(uint8_t const number,
760@@ -1312,7 +1268,7 @@
761 // uses it.
762 }
763
764-void GameHost::set_player_shared(uint8_t number, uint8_t shared) {
765+void GameHost::set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) {
766 if (number >= d->settings.players.size())
767 return;
768
769@@ -1322,8 +1278,8 @@
770 return;
771
772 PlayerSettings& sharedplr = d->settings.players.at(shared - 1);
773- assert(sharedplr.state != PlayerSettings::stateClosed &&
774- sharedplr.state != PlayerSettings::stateShared);
775+ assert(PlayerSettings::can_be_shared(sharedplr.state));
776+ assert(d->settings.is_shared_usable(number, shared));
777
778 player.shared_in = shared;
779 player.tribe = sharedplr.tribe;
780@@ -1367,8 +1323,8 @@
781
782 void GameHost::switch_to_player(uint32_t user, uint8_t number) {
783 if (number < d->settings.players.size() &&
784- (d->settings.players.at(number).state != PlayerSettings::stateOpen &&
785- d->settings.players.at(number).state != PlayerSettings::stateHuman))
786+ (d->settings.players.at(number).state != PlayerSettings::State::kOpen &&
787+ d->settings.players.at(number).state != PlayerSettings::State::kHuman))
788 return;
789
790 uint32_t old = d->settings.users.at(user).position;
791@@ -1383,14 +1339,14 @@
792 temp2 = temp2.erase(op.name.find(temp), temp.size());
793 set_player_name(old, temp2);
794 if (temp2.empty())
795- set_player_state(old, PlayerSettings::stateOpen);
796+ set_player_state(old, PlayerSettings::State::kOpen);
797 }
798
799 if (number < d->settings.players.size()) {
800 // Add clients name to new player slot
801 PlayerSettings& op = d->settings.players.at(number);
802- if (op.state == PlayerSettings::stateOpen) {
803- set_player_state(number, PlayerSettings::stateHuman);
804+ if (op.state == PlayerSettings::State::kOpen) {
805+ set_player_state(number, PlayerSettings::State::kHuman);
806 set_player_name(number, " " + name + " ");
807 } else
808 set_player_name(number, op.name + " " + name + " ");
809@@ -1477,6 +1433,7 @@
810 packet.string(d->settings.mapfilename);
811 packet.unsigned_8(d->settings.savegame ? 1 : 0);
812 packet.unsigned_8(d->settings.scenario ? 1 : 0);
813+ Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kMap));
814 }
815
816 void GameHost::write_setting_player(SendPacket& packet, uint8_t const number) {
817@@ -1490,6 +1447,7 @@
818 packet.unsigned_8(player.random_ai ? 1 : 0);
819 packet.unsigned_8(player.team);
820 packet.unsigned_8(player.shared_in);
821+ Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kPlayer, number));
822 }
823
824 void GameHost::write_setting_all_players(SendPacket& packet) {
825@@ -1502,6 +1460,8 @@
826 packet.string(d->settings.users.at(number).name);
827 packet.signed_32(d->settings.users.at(number).position);
828 packet.unsigned_8(d->settings.users.at(number).ready ? 1 : 0);
829+ Notifications::publish(NoteGameSettings(
830+ NoteGameSettings::Action::kUser, d->settings.users.at(number).position, number));
831 }
832
833 void GameHost::write_setting_all_users(SendPacket& packet) {
834@@ -1673,7 +1633,7 @@
835
836 // Check if there is an unoccupied player left and if, assign.
837 for (uint8_t i = 0; i < d->settings.players.size(); ++i)
838- if (d->settings.players.at(i).state == PlayerSettings::stateOpen) {
839+ if (d->settings.players.at(i).state == PlayerSettings::State::kOpen) {
840 switch_to_player(client.usernum, i);
841 break;
842 }
843@@ -2096,10 +2056,13 @@
844
845 case NETCMD_SETTING_CHANGEINIT:
846 if (!d->game) {
847+ // TODO(GunChleoc): For some nebulous reason, we don't receive the num that the client is
848+ // sending when a player changes slot. So, keeping the access to the client off for now.
849+ // Would be nice to have though.
850 uint8_t num = r.unsigned_8();
851 if (num != client.playernum)
852 throw DisconnectException("NO_ACCESS_TO_PLAYER");
853- d->npsb.toggle_init(num);
854+ set_player_init(num, r.unsigned_8());
855 }
856 break;
857
858@@ -2267,7 +2230,7 @@
859 }
860 }
861
862- set_player_state(number, PlayerSettings::stateOpen);
863+ set_player_state(number, PlayerSettings::State::kOpen);
864 if (d->game)
865 init_computer_player(number + 1);
866 }
867
868=== modified file 'src/network/gamehost.h'
869--- src/network/gamehost.h 2017-07-02 21:09:23 +0000
870+++ src/network/gamehost.h 2017-09-02 13:55:49 +0000
871@@ -76,7 +76,7 @@
872 void set_player_number(uint8_t number);
873 void set_player_team(uint8_t number, Widelands::TeamNumber team);
874 void set_player_closeable(uint8_t number, bool closeable);
875- void set_player_shared(uint8_t number, uint8_t shared);
876+ void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared);
877 void switch_to_player(uint32_t user, uint8_t number);
878 void set_win_condition_script(const std::string& wc);
879
880
881=== modified file 'src/network/network_player_settings_backend.cc'
882--- src/network/network_player_settings_backend.cc 2017-02-14 10:18:15 +0000
883+++ src/network/network_player_settings_backend.cc 2017-09-02 13:55:49 +0000
884@@ -19,134 +19,69 @@
885
886 #include "network/network_player_settings_backend.h"
887
888-#include "base/i18n.h"
889-#include "base/log.h"
890-#include "base/wexception.h"
891-#include "logic/game_settings.h"
892-#include "logic/map_objects/tribes/tribe_descr.h"
893-#include "logic/player.h"
894-
895-/// Toggle through the types
896-void NetworkPlayerSettingsBackend::toggle_type(uint8_t id) {
897- if (id >= s->settings().players.size())
898- return;
899-
900- s->next_player_state(id);
901-}
902-
903-void NetworkPlayerSettingsBackend::set_tribe(uint8_t id, const std::string& tribename) {
904+#include "ai/computer_player.h"
905+
906+void NetworkPlayerSettingsBackend::set_player_state(PlayerSlot id, PlayerSettings::State state) {
907+ if (id >= s->settings().players.size()) {
908+ return;
909+ }
910+ s->set_player_state(id, state);
911+}
912+
913+void NetworkPlayerSettingsBackend::set_player_ai(PlayerSlot id,
914+ const std::string& name,
915+ bool random_ai) {
916+ if (id >= s->settings().players.size()) {
917+ return;
918+ }
919+ if (random_ai) {
920+ const ComputerPlayer::ImplementationVector& impls = ComputerPlayer::get_implementations();
921+ ComputerPlayer::ImplementationVector::const_iterator it = impls.begin();
922+ if (impls.size() > 1) {
923+ do {
924+ size_t random = (std::rand() % impls.size()); // Choose a random AI
925+ it = impls.begin() + random;
926+ } while ((*it)->type == ComputerPlayer::Implementation::Type::kEmpty);
927+ }
928+ s->set_player_ai(id, (*it)->name, random_ai);
929+ } else {
930+ s->set_player_ai(id, name, random_ai);
931+ }
932+}
933+
934+void NetworkPlayerSettingsBackend::set_player_tribe(PlayerSlot id, const std::string& tribename) {
935 const GameSettings& settings = s->settings();
936-
937- if (id >= settings.players.size() || tribename.empty())
938+ if (id >= settings.players.size() || tribename.empty()) {
939 return;
940-
941- if (settings.players.at(id).state != PlayerSettings::stateShared) {
942+ }
943+ if (settings.players.at(id).state != PlayerSettings::State::kShared) {
944 s->set_player_tribe(id, tribename, tribename == "random");
945 }
946 }
947
948 /// Set the shared in player for the given id
949-void NetworkPlayerSettingsBackend::set_shared_in(uint8_t id, uint8_t shared_in) {
950- const GameSettings& settings = s->settings();
951- if (id > settings.players.size() || shared_in > settings.players.size())
952- return;
953- if (settings.players.at(id).state == PlayerSettings::stateShared) {
954- s->set_player_shared(id, shared_in);
955- }
956-}
957-
958-/// Toggle through shared in players
959-void NetworkPlayerSettingsBackend::toggle_shared_in(uint8_t id) {
960- const GameSettings& settings = s->settings();
961-
962- if (id >= settings.players.size() ||
963- settings.players.at(id).state != PlayerSettings::stateShared)
964- return;
965-
966- uint8_t sharedplr = settings.players.at(id).shared_in;
967- for (; sharedplr < settings.players.size(); ++sharedplr) {
968- if (settings.players.at(sharedplr).state != PlayerSettings::stateClosed &&
969- settings.players.at(sharedplr).state != PlayerSettings::stateShared)
970- break;
971- }
972- if (sharedplr < settings.players.size()) {
973- // We have already found the next player
974- set_shared_in(id, sharedplr + 1);
975- return;
976- }
977- sharedplr = 0;
978- for (; sharedplr < settings.players.at(id).shared_in; ++sharedplr) {
979- if (settings.players.at(sharedplr).state != PlayerSettings::stateClosed &&
980- settings.players.at(sharedplr).state != PlayerSettings::stateShared)
981- break;
982- }
983- if (sharedplr < settings.players.at(id).shared_in) {
984- // We have found the next player
985- set_shared_in(id, sharedplr + 1);
986- return;
987- } else {
988- // No fitting player found
989- return toggle_type(id);
990- }
991-}
992-
993-/// Toggle through the initializations
994-void NetworkPlayerSettingsBackend::toggle_init(uint8_t id) {
995- const GameSettings& settings = s->settings();
996-
997- if (id >= settings.players.size())
998- return;
999-
1000- const PlayerSettings& player = settings.players[id];
1001- for (const TribeBasicInfo& temp_tribeinfo : settings.tribes) {
1002- if (temp_tribeinfo.name == player.tribe) {
1003- return s->set_player_init(
1004- id, (player.initialization_index + 1) % temp_tribeinfo.initializations.size());
1005- }
1006- }
1007- NEVER_HERE();
1008-}
1009-
1010-/// Toggle through the teams
1011-void NetworkPlayerSettingsBackend::toggle_team(uint8_t id) {
1012- const GameSettings& settings = s->settings();
1013-
1014- if (id >= settings.players.size())
1015- return;
1016-
1017- Widelands::TeamNumber currentteam = settings.players.at(id).team;
1018- Widelands::TeamNumber maxteam = settings.players.size() / 2;
1019- Widelands::TeamNumber newteam;
1020-
1021- if (currentteam >= maxteam)
1022- newteam = 0;
1023- else
1024- newteam = currentteam + 1;
1025-
1026- s->set_player_team(id, newteam);
1027-}
1028-
1029-/// Check if all settings for the player are still valid
1030-void NetworkPlayerSettingsBackend::refresh(uint8_t id) {
1031- const GameSettings& settings = s->settings();
1032-
1033- if (id >= settings.players.size())
1034- return;
1035-
1036- const PlayerSettings& player = settings.players[id];
1037-
1038- if (player.state == PlayerSettings::stateShared) {
1039- // ensure that the shared_in player is able to use this starting position
1040- if (player.shared_in > settings.players.size())
1041- toggle_shared_in(id);
1042- if (settings.players.at(player.shared_in - 1).state == PlayerSettings::stateClosed ||
1043- settings.players.at(player.shared_in - 1).state == PlayerSettings::stateShared)
1044- toggle_shared_in(id);
1045-
1046- if (shared_in_tribe[id] != settings.players.at(player.shared_in - 1).tribe) {
1047- s->set_player_tribe(id, settings.players.at(player.shared_in - 1).tribe,
1048- settings.players.at(player.shared_in - 1).random_tribe);
1049- shared_in_tribe[id] = settings.players.at(id).tribe;
1050- }
1051- }
1052+void NetworkPlayerSettingsBackend::set_player_shared(PlayerSlot id,
1053+ Widelands::PlayerNumber shared) {
1054+ const GameSettings& settings = s->settings();
1055+ if (id >= settings.players.size() || shared > settings.players.size())
1056+ return;
1057+ if (settings.players.at(id).state == PlayerSettings::State::kShared) {
1058+ s->set_player_shared(id, shared);
1059+ }
1060+}
1061+
1062+/// Sets the initialization for the player slot (Headquarters, Fortified Village etc.)
1063+void NetworkPlayerSettingsBackend::set_player_init(PlayerSlot id, uint8_t initialization_index) {
1064+ if (id >= s->settings().players.size()) {
1065+ return;
1066+ }
1067+ s->set_player_init(id, initialization_index);
1068+}
1069+
1070+/// Sets the team for the player slot
1071+void NetworkPlayerSettingsBackend::set_player_team(PlayerSlot id, Widelands::TeamNumber team) {
1072+ if (id >= s->settings().players.size()) {
1073+ return;
1074+ }
1075+ s->set_player_team(id, team);
1076 }
1077
1078=== modified file 'src/network/network_player_settings_backend.h'
1079--- src/network/network_player_settings_backend.h 2017-08-16 10:14:29 +0000
1080+++ src/network/network_player_settings_backend.h 2017-09-02 13:55:49 +0000
1081@@ -20,33 +20,22 @@
1082 #ifndef WL_NETWORK_NETWORK_PLAYER_SETTINGS_BACKEND_H
1083 #define WL_NETWORK_NETWORK_PLAYER_SETTINGS_BACKEND_H
1084
1085-#include "graphic/playercolor.h"
1086 #include "logic/game_settings.h"
1087+#include "logic/widelands.h"
1088
1089 struct NetworkPlayerSettingsBackend {
1090
1091 explicit NetworkPlayerSettingsBackend(GameSettingsProvider* const settings) : s(settings) {
1092- for (uint8_t i = 0; i < kMaxPlayers; ++i)
1093- shared_in_tribe[i] = std::string();
1094- }
1095-
1096- void toggle_type(uint8_t id);
1097- void set_shared_in(uint8_t id, uint8_t shared_in);
1098- void set_tribe(uint8_t id, const std::string& tribename);
1099- void set_block_tribe_selection(bool blocked) {
1100- tribe_selection_blocked = blocked;
1101- }
1102-
1103- void toggle_init(uint8_t id);
1104- void toggle_team(uint8_t id);
1105- void refresh(uint8_t id);
1106+ }
1107+
1108+ void set_player_state(PlayerSlot id, PlayerSettings::State state);
1109+ void set_player_ai(PlayerSlot id, const std::string& name, bool random_ai);
1110+ void set_player_shared(PlayerSlot id, Widelands::PlayerNumber shared);
1111+ void set_player_tribe(PlayerSlot id, const std::string& tribename);
1112+ void set_player_init(PlayerSlot id, uint8_t initialization_index);
1113+ void set_player_team(PlayerSlot id, Widelands::TeamNumber team);
1114
1115 GameSettingsProvider* const s;
1116- std::string shared_in_tribe[kMaxPlayers];
1117- bool tribe_selection_blocked = false;
1118-
1119-private:
1120- void toggle_shared_in(uint8_t id);
1121 };
1122
1123 #endif // end of include guard: WL_NETWORK_NETWORK_PLAYER_SETTINGS_BACKEND_H
1124
1125=== modified file 'src/network/network_protocol.h'
1126--- src/network/network_protocol.h 2017-05-06 11:00:19 +0000
1127+++ src/network/network_protocol.h 2017-09-02 13:55:49 +0000
1128@@ -395,9 +395,9 @@
1129 * \li unsigned_8: new shared player
1130 *
1131 * \note The client must not assume that the host will accept this
1132- * request. Change of team number only becomes effective when/if the host
1133+ * request. Change of the initialization only becomes effective when/if the host
1134 * replies with a \ref NETCMD_SETTING_PLAYER or
1135- * \ref NETCMD_SETTING_ALLPLAYERS indicating the changed team.
1136+ * \ref NETCMD_SETTING_ALLPLAYERS indicating the changed initialization.
1137 */
1138 NETCMD_SETTING_CHANGESHARED = 27,
1139
1140@@ -406,6 +406,7 @@
1141 * client wants to change a player's initialisation.
1142 *
1143 * \li unsigned_8: number of the player
1144+ * \li unsigned_8: index of the initialization
1145 *
1146 * \note The client must not assume that the host will accept this
1147 * request. Change of team number only becomes effective when/if the host
1148
1149=== modified file 'src/notifications/note_ids.h'
1150--- src/notifications/note_ids.h 2017-08-30 07:45:52 +0000
1151+++ src/notifications/note_ids.h 2017-09-02 13:55:49 +0000
1152@@ -39,7 +39,9 @@
1153 Economy,
1154 GraphicResolutionChanged,
1155 NoteExpeditionCanceled,
1156- Sound
1157+ Sound,
1158+ Dropdown,
1159+ GameSettings
1160 };
1161
1162 #endif // end of include guard: WL_NOTIFICATIONS_NOTE_IDS_H
1163
1164=== modified file 'src/ui_basic/dropdown.cc'
1165--- src/ui_basic/dropdown.cc 2017-06-02 08:14:40 +0000
1166+++ src/ui_basic/dropdown.cc 2017-09-02 13:55:49 +0000
1167@@ -43,6 +43,8 @@
1168
1169 namespace UI {
1170
1171+int BaseDropdown::next_id_ = 0;
1172+
1173 BaseDropdown::BaseDropdown(UI::Panel* parent,
1174 int32_t x,
1175 int32_t y,
1176@@ -56,11 +58,14 @@
1177 : UI::Panel(parent,
1178 x,
1179 y,
1180- type == DropdownType::kTextual ? w : button_dimension,
1181+ type == DropdownType::kPictorial ? button_dimension : w,
1182 // Height only to fit the button, so we can use this in Box layout.
1183 base_height(button_dimension)),
1184+ id_(next_id_++),
1185 max_list_height_(h - 2 * get_h()),
1186 list_width_(w),
1187+ list_offset_x_(0),
1188+ list_offset_y_(0),
1189 button_dimension_(button_dimension),
1190 mouse_tolerance_(50),
1191 button_box_(this, 0, 0, UI::Box::Horizontal, w, h),
1192@@ -79,13 +84,23 @@
1193 "dropdown_label",
1194 0,
1195 0,
1196- type == DropdownType::kTextual ? w - button_dimension : button_dimension,
1197+ type == DropdownType::kTextual ?
1198+ w - button_dimension :
1199+ type == DropdownType::kTextualNarrow ? w : button_dimension,
1200 get_h(),
1201 background,
1202 label),
1203 label_(label),
1204 type_(type),
1205 is_enabled_(true) {
1206+
1207+ // Close whenever another dropdown is opened
1208+ subscriber_ = Notifications::subscribe<NoteDropdown>([this](const NoteDropdown& note) {
1209+ if (id_ != note.id) {
1210+ close();
1211+ }
1212+ });
1213+
1214 assert(max_list_height_ > 0);
1215 // Hook into highest parent that we can get so that we can drop down outside the panel.
1216 // Positioning breaks down with TabPanels, so we exclude them.
1217@@ -125,26 +140,42 @@
1218
1219 void BaseDropdown::layout() {
1220 const int base_h = base_height(button_dimension_);
1221- const int w = type_ == DropdownType::kTextual ? get_w() : button_dimension_;
1222+ const int w = type_ == DropdownType::kPictorial ? button_dimension_ : get_w();
1223 button_box_.set_size(w, base_h);
1224 display_button_.set_desired_size(
1225 type_ == DropdownType::kTextual ? w - button_dimension_ : w, base_h);
1226 int new_list_height =
1227 std::min(static_cast<int>(list_->size()) * list_->get_lineheight(), max_list_height_);
1228- list_->set_size(type_ == DropdownType::kTextual ? w : list_width_, new_list_height);
1229+ list_->set_size(type_ != DropdownType::kPictorial ? w : list_width_, new_list_height);
1230 set_desired_size(w, base_h);
1231
1232 // Update list position. The list is hooked into the highest parent that we can get so that we
1233 // can drop down outside the panel. Positioning breaks down with TabPanels, so we exclude them.
1234 UI::Panel* parent = get_parent();
1235- int new_list_y = get_y() + get_h() + parent->get_y();
1236+ int new_list_y = get_y() + parent->get_y();
1237 int new_list_x = get_x() + parent->get_x();
1238 while (parent->get_parent() && !is_a(UI::TabPanel, parent->get_parent())) {
1239 parent = parent->get_parent();
1240 new_list_y += parent->get_y();
1241 new_list_x += parent->get_x();
1242 }
1243- list_->set_pos(Vector2i(new_list_x, new_list_y));
1244+
1245+ // Drop up instead of down if it doesn't fit
1246+ if (new_list_y + list_->get_h() > g_gr->get_yres()) {
1247+ list_offset_y_ = -list_->get_h();
1248+ } else {
1249+ list_offset_y_ = display_button_.get_h();
1250+ }
1251+
1252+ // Right align instead of left align if it doesn't fit
1253+ if (new_list_x + list_->get_w() > g_gr->get_xres()) {
1254+ list_offset_x_ = display_button_.get_w() - list_->get_w();
1255+ if (push_button_ != nullptr) {
1256+ list_offset_x_ += push_button_->get_w();
1257+ }
1258+ }
1259+
1260+ list_->set_pos(Vector2i(new_list_x + list_offset_x_, new_list_y + list_offset_y_));
1261 }
1262
1263 void BaseDropdown::add(const std::string& name,
1264@@ -178,16 +209,29 @@
1265
1266 void BaseDropdown::set_label(const std::string& text) {
1267 label_ = text;
1268- if (type_ == DropdownType::kTextual) {
1269+ if (type_ != DropdownType::kPictorial) {
1270 display_button_.set_title(label_);
1271 }
1272 }
1273
1274+void BaseDropdown::set_image(const Image* image) {
1275+ display_button_.set_pic(image);
1276+}
1277+
1278 void BaseDropdown::set_tooltip(const std::string& text) {
1279 tooltip_ = text;
1280 display_button_.set_tooltip(tooltip_);
1281 }
1282
1283+void BaseDropdown::set_errored(const std::string& error_message) {
1284+ set_tooltip((boost::format(_("%1%: %2%")) % _("Error") % error_message).str());
1285+ if (type_ != DropdownType::kPictorial) {
1286+ set_label(_("Error"));
1287+ } else {
1288+ set_image(g_gr->images().get("images/ui_basic/different.png"));
1289+ }
1290+}
1291+
1292 void BaseDropdown::set_enabled(bool on) {
1293 is_enabled_ = on;
1294 set_can_focus(on);
1295@@ -213,6 +257,7 @@
1296 }
1297
1298 void BaseDropdown::clear() {
1299+ close();
1300 list_->clear();
1301 current_selection_ = list_->selection_index();
1302 list_->set_size(list_->get_w(), 0);
1303@@ -239,7 +284,7 @@
1304 /** TRANSLATORS: Selection in Dropdown menus. */
1305 pgettext("dropdown", "Not Selected");
1306
1307- if (type_ == DropdownType::kTextual) {
1308+ if (type_ != DropdownType::kPictorial) {
1309 if (label_.empty()) {
1310 display_button_.set_title(name);
1311 } else {
1312@@ -268,19 +313,29 @@
1313 return;
1314 }
1315 list_->set_visible(!list_->is_visible());
1316+ if (type_ != DropdownType::kTextual) {
1317+ display_button_.set_perm_pressed(list_->is_visible());
1318+ }
1319 if (list_->is_visible()) {
1320 list_->move_to_top();
1321 focus();
1322+ Notifications::publish(NoteDropdown(id_));
1323 }
1324 // Make sure that the list covers and deactivates the elements below it
1325 set_layout_toplevel(list_->is_visible());
1326 }
1327
1328+void BaseDropdown::close() {
1329+ if (is_expanded()) {
1330+ toggle_list();
1331+ }
1332+}
1333+
1334 bool BaseDropdown::is_mouse_away() const {
1335- return (get_mouse_position().x + mouse_tolerance_) < 0 ||
1336- get_mouse_position().x > (list_->get_w() + mouse_tolerance_) ||
1337- (get_mouse_position().y + mouse_tolerance_ / 2) < 0 ||
1338- get_mouse_position().y > (get_h() + list_->get_h() + mouse_tolerance_);
1339+ return (get_mouse_position().x + mouse_tolerance_) < list_offset_x_ ||
1340+ get_mouse_position().x > (list_offset_x_ + list_->get_w() + mouse_tolerance_) ||
1341+ (get_mouse_position().y + mouse_tolerance_) < list_offset_y_ ||
1342+ get_mouse_position().y > (list_offset_y_ + get_h() + list_->get_h() + mouse_tolerance_);
1343 }
1344
1345 bool BaseDropdown::handle_key(bool down, SDL_Keysym code) {
1346
1347=== modified file 'src/ui_basic/dropdown.h'
1348--- src/ui_basic/dropdown.h 2017-04-21 09:56:42 +0000
1349+++ src/ui_basic/dropdown.h 2017-09-02 13:55:49 +0000
1350@@ -27,14 +27,26 @@
1351
1352 #include "graphic/graphic.h"
1353 #include "graphic/image.h"
1354+#include "notifications/note_ids.h"
1355+#include "notifications/notifications.h"
1356 #include "ui_basic/box.h"
1357 #include "ui_basic/button.h"
1358 #include "ui_basic/listselect.h"
1359 #include "ui_basic/panel.h"
1360
1361 namespace UI {
1362-
1363-enum class DropdownType { kTextual, kPictorial };
1364+// We use this to make sure that only 1 dropdown is open at the same time.
1365+struct NoteDropdown {
1366+ CAN_BE_SENT_AS_NOTE(NoteId::Dropdown)
1367+
1368+ int id;
1369+
1370+ explicit NoteDropdown(int init_id) : id(init_id) {
1371+ }
1372+};
1373+
1374+/// The narrow textual dropdown omits the extra push button
1375+enum class DropdownType { kTextual, kTextualNarrow, kPictorial };
1376
1377 /// Implementation for a dropdown menu that lets the user select a value.
1378 class BaseDropdown : public Panel {
1379@@ -63,6 +75,7 @@
1380 ~BaseDropdown();
1381
1382 public:
1383+ /// An entry was selected
1384 boost::signals2::signal<void()> selected;
1385
1386 /// \return true if an element has been selected from the list
1387@@ -72,9 +85,15 @@
1388 /// and displayed on the display button.
1389 void set_label(const std::string& text);
1390
1391+ /// Sets the image for the display button (for pictorial dropdowns).
1392+ void set_image(const Image* image);
1393+
1394 /// Sets the tooltip for the display button.
1395 void set_tooltip(const std::string& text);
1396
1397+ /// Displays an error message on the button instead of the current selection.
1398+ void set_errored(const std::string& error_message);
1399+
1400 /// Enables/disables the dropdown selection.
1401 void set_enabled(bool on);
1402
1403@@ -142,12 +161,22 @@
1404 void set_value();
1405 /// Toggles the dropdown list on and off.
1406 void toggle_list();
1407+ /// Toggle the list closed if the dropdown is currently expanded.
1408+ void close();
1409
1410 /// Returns true if the mouse pointer left the vicinity of the dropdown.
1411 bool is_mouse_away() const;
1412
1413+ /// Give each dropdown a unique ID
1414+ static int next_id_;
1415+ const int id_;
1416+ std::unique_ptr<Notifications::Subscriber<NoteDropdown>> subscriber_;
1417+
1418+ // Dimensions
1419 int max_list_height_;
1420 int list_width_;
1421+ int list_offset_x_;
1422+ int list_offset_y_;
1423 int button_dimension_;
1424 const int mouse_tolerance_; // Allow mouse outside the panel a bit before autocollapse
1425 UI::Box button_box_;
1426
1427=== modified file 'src/ui_basic/listselect.cc'
1428--- src/ui_basic/listselect.cc 2017-06-15 16:34:58 +0000
1429+++ src/ui_basic/listselect.cc 2017-09-02 13:55:49 +0000
1430@@ -359,9 +359,9 @@
1431 if (selection_mode_ == ListselectLayout::kDropdown) {
1432 RGBAColor black(0, 0, 0, 255);
1433 // top edge
1434- dst.brighten_rect(Recti(0, 0, get_w(), 2), BUTTON_EDGE_BRIGHT_FACTOR / 4);
1435+ dst.brighten_rect(Recti(0, 0, get_w(), 2), BUTTON_EDGE_BRIGHT_FACTOR);
1436 // left edge
1437- dst.brighten_rect(Recti(0, 0, 2, get_h()), BUTTON_EDGE_BRIGHT_FACTOR);
1438+ dst.brighten_rect(Recti(0, 2, 2, get_h()), BUTTON_EDGE_BRIGHT_FACTOR);
1439 // bottom edge
1440 dst.fill_rect(Recti(2, get_h() - 2, get_eff_w() - 2, 1), black);
1441 dst.fill_rect(Recti(1, get_h() - 1, get_eff_w() - 1, 1), black);
1442
1443=== modified file 'src/ui_fsmenu/launch_game.cc'
1444--- src/ui_fsmenu/launch_game.cc 2017-05-18 06:52:04 +0000
1445+++ src/ui_fsmenu/launch_game.cc 2017-09-02 13:55:49 +0000
1446@@ -167,7 +167,7 @@
1447 t->get_string("description"));
1448 }
1449 } catch (LuaTableKeyError& e) {
1450- log("LaunchSPG: Error loading win condition: %s %s\n", win_condition_script.c_str(),
1451+ log("Launch Game: Error loading win condition: %s %s\n", win_condition_script.c_str(),
1452 e.what());
1453 }
1454 }
1455@@ -177,9 +177,8 @@
1456 "could not be loaded.")) %
1457 settings_->settings().mapfilename)
1458 .str();
1459- win_condition_dropdown_.set_label(_("Error"));
1460- win_condition_dropdown_.set_tooltip(error_message);
1461- log("LaunchSPG: Exception: %s %s\n", error_message.c_str(), e.what());
1462+ win_condition_dropdown_.set_errored(error_message);
1463+ log("Launch Game: Exception: %s %s\n", error_message.c_str(), e.what());
1464 }
1465 }
1466
1467@@ -202,8 +201,8 @@
1468 }
1469 }
1470 } catch (LuaTableKeyError& e) {
1471- log(
1472- "LaunchSPG: Error loading win condition: %s %s\n", win_condition_script.c_str(), e.what());
1473+ log("Launch Game: Error loading win condition: %s %s\n", win_condition_script.c_str(),
1474+ e.what());
1475 }
1476 if (!is_usable) {
1477 t.reset(nullptr);
1478
1479=== modified file 'src/ui_fsmenu/launch_mpg.cc'
1480--- src/ui_fsmenu/launch_mpg.cc 2017-05-18 06:52:04 +0000
1481+++ src/ui_fsmenu/launch_mpg.cc 2017-09-02 13:55:49 +0000
1482@@ -181,7 +181,7 @@
1483 map_info_.set_text(_("The host has not yet selected a map or saved game."));
1484
1485 mpsg_ = new MultiPlayerSetupGroup(
1486- this, get_w() / 50, get_h() / 8, get_w() * 57 / 80, get_h(), settings, butw_, buth_);
1487+ this, get_w() / 50, change_map_or_save_.get_y(), get_w() * 57 / 80, get_h(), settings, buth_);
1488
1489 // If we are the host, open the map or save selection menu at startup
1490 if (settings_->settings().usernum == 0 && settings_->settings().mapname.empty()) {
1491@@ -355,6 +355,8 @@
1492 * buttons and text.
1493 */
1494 void FullscreenMenuLaunchMPG::refresh() {
1495+ // TODO(GunChleoc): Investigate what we can handle with NoteGameSettings. Maybe we can get rid of
1496+ // refresh() and thus think().
1497 const GameSettings& settings = settings_->settings();
1498
1499 if (settings.mapfilename != filename_proof_) {
1500@@ -425,8 +427,6 @@
1501 }
1502 win_condition_dropdown_.set_enabled(false);
1503 }
1504- // Update the multi player setup group
1505- mpsg_->refresh();
1506 }
1507
1508 /**
1509@@ -443,16 +443,21 @@
1510 map.set_filename(settings.mapfilename);
1511 ml->preload_map(true);
1512 Widelands::PlayerNumber const nrplayers = map.get_nrplayers();
1513+ if (settings.players.size() != nrplayers) {
1514+ // Due to asynchronous notifications, the client can crash when an update is missing and the
1515+ // number of players is wrong.
1516+ return;
1517+ }
1518 for (uint8_t i = 0; i < nrplayers; ++i) {
1519 settings_->set_player_tribe(i, map.get_scenario_player_tribe(i + 1));
1520 settings_->set_player_closeable(i, map.get_scenario_player_closeable(i + 1));
1521 std::string ai(map.get_scenario_player_ai(i + 1));
1522 if (!ai.empty()) {
1523- settings_->set_player_state(i, PlayerSettings::stateComputer);
1524+ settings_->set_player_state(i, PlayerSettings::State::kComputer);
1525 settings_->set_player_ai(i, ai);
1526- } else if (settings.players.at(i).state != PlayerSettings::stateHuman &&
1527- settings.players.at(i).state != PlayerSettings::stateOpen) {
1528- settings_->set_player_state(i, PlayerSettings::stateOpen);
1529+ } else if (settings.players.at(i).state != PlayerSettings::State::kHuman &&
1530+ settings.players.at(i).state != PlayerSettings::State::kOpen) {
1531+ settings_->set_player_state(i, PlayerSettings::State::kOpen);
1532 }
1533 }
1534 }
1535@@ -470,22 +475,25 @@
1536 std::string player_save_tribe[kMaxPlayers];
1537 std::string player_save_ai[kMaxPlayers];
1538
1539- uint8_t i = 1;
1540- for (; i <= nr_players_; ++i) {
1541+ for (uint8_t i = 1; i <= nr_players_; ++i) {
1542+ Section* s = prof.get_section((boost::format("player_%u") % static_cast<unsigned int>(i)).str());
1543+ if (s == nullptr) {
1544+ // Due to asynchronous notifications, the client can crash on savegame change when number
1545+ // of players goes down. So, we abort if the section does not exist to prevent crashes.
1546+ return;
1547+ }
1548+ player_save_name[i - 1] = s->get_string("name");
1549+ player_save_tribe[i - 1] = s->get_string("tribe");
1550+ player_save_ai[i - 1] = s->get_string("ai");
1551+
1552 infotext += "\n* ";
1553- Section& s =
1554- prof.get_safe_section((boost::format("player_%u") % static_cast<unsigned int>(i)).str());
1555- player_save_name[i - 1] = s.get_string("name");
1556- player_save_tribe[i - 1] = s.get_string("tribe");
1557- player_save_ai[i - 1] = s.get_string("ai");
1558-
1559 infotext += (boost::format(_("Player %u")) % static_cast<unsigned int>(i)).str();
1560 if (player_save_tribe[i - 1].empty()) {
1561 std::string closed_string = (boost::format("<%s>") % _("closed")).str();
1562 infotext += ":\n ";
1563 infotext += closed_string;
1564 // Close the player
1565- settings_->set_player_state(i - 1, PlayerSettings::stateClosed);
1566+ settings_->set_player_state(i - 1, PlayerSettings::State::kClosed);
1567 continue; // if tribe is empty, the player does not exist
1568 }
1569
1570@@ -495,10 +503,10 @@
1571
1572 if (player_save_ai[i - 1].empty()) {
1573 // Assure that player is open
1574- if (settings_->settings().players.at(i - 1).state != PlayerSettings::stateHuman)
1575- settings_->set_player_state(i - 1, PlayerSettings::stateOpen);
1576+ if (settings_->settings().players.at(i - 1).state != PlayerSettings::State::kHuman)
1577+ settings_->set_player_state(i - 1, PlayerSettings::State::kOpen);
1578 } else {
1579- settings_->set_player_state(i - 1, PlayerSettings::stateComputer);
1580+ settings_->set_player_state(i - 1, PlayerSettings::State::kComputer);
1581 settings_->set_player_ai(i - 1, player_save_ai[i - 1]);
1582 }
1583
1584@@ -563,7 +571,9 @@
1585 "\n";
1586 infotext +=
1587 std::string("• ") +
1588- (boost::format(ngettext("%u Player", "%u Players", nr_players_)) % nr_players_).str() + "\n";
1589+ (boost::format(ngettext("%u Player", "%u Players", nr_players_)) % static_cast<unsigned int>(nr_players_))
1590+ .str() +
1591+ "\n";
1592 if (settings_->settings().scenario)
1593 infotext += std::string("• ") + (boost::format(_("Scenario mode selected"))).str() + "\n";
1594 infotext += "\n";
1595
1596=== modified file 'src/ui_fsmenu/launch_mpg.h'
1597--- src/ui_fsmenu/launch_mpg.h 2017-02-10 14:12:36 +0000
1598+++ src/ui_fsmenu/launch_mpg.h 2017-09-02 13:55:49 +0000
1599@@ -23,6 +23,7 @@
1600 #include <memory>
1601 #include <string>
1602
1603+#include "logic/game_settings.h"
1604 #include "ui_basic/button.h"
1605 #include "ui_basic/dropdown.h"
1606 #include "ui_basic/multilinetextarea.h"
1607
1608=== modified file 'src/ui_fsmenu/launch_spg.cc'
1609--- src/ui_fsmenu/launch_spg.cc 2017-05-04 05:08:14 +0000
1610+++ src/ui_fsmenu/launch_spg.cc 2017-09-02 13:55:49 +0000
1611@@ -210,8 +210,8 @@
1612 for (uint8_t i = 0; i < nr_players_; ++i) {
1613 pos_[i]->set_visible(true);
1614 const PlayerSettings& player = settings.players[i];
1615- pos_[i]->set_enabled(!is_scenario_ && (player.state == PlayerSettings::stateOpen ||
1616- player.state == PlayerSettings::stateComputer));
1617+ pos_[i]->set_enabled(!is_scenario_ && (player.state == PlayerSettings::State::kOpen ||
1618+ player.state == PlayerSettings::State::kComputer));
1619 }
1620 for (uint32_t i = nr_players_; i < kMaxPlayers; ++i)
1621 pos_[i]->set_visible(false);
1622@@ -291,14 +291,14 @@
1623 // Check if a still valid place is open.
1624 for (uint8_t i = 0; i < newplayernumber; ++i) {
1625 PlayerSettings position = settings.players.at(i);
1626- if (position.state == PlayerSettings::stateOpen) {
1627+ if (position.state == PlayerSettings::State::kOpen) {
1628 switch_to_position(i);
1629 return;
1630 }
1631 }
1632
1633 // Kick player 1 and take the position
1634- settings_->set_player_state(0, PlayerSettings::stateClosed);
1635- settings_->set_player_state(0, PlayerSettings::stateOpen);
1636+ settings_->set_player_state(0, PlayerSettings::State::kClosed);
1637+ settings_->set_player_state(0, PlayerSettings::State::kOpen);
1638 switch_to_position(0);
1639 }
1640
1641=== modified file 'src/wui/multiplayersetupgroup.cc'
1642--- src/wui/multiplayersetupgroup.cc 2017-05-03 07:24:06 +0000
1643+++ src/wui/multiplayersetupgroup.cc 2017-09-02 13:55:49 +0000
1644@@ -19,8 +19,10 @@
1645
1646 #include "wui/multiplayersetupgroup.h"
1647
1648+#include <memory>
1649 #include <string>
1650
1651+#include <boost/algorithm/string.hpp>
1652 #include <boost/format.hpp>
1653 #include <boost/lexical_cast.hpp>
1654
1655@@ -36,419 +38,550 @@
1656 #include "logic/map_objects/tribes/tribe_descr.h"
1657 #include "logic/map_objects/tribes/tribes.h"
1658 #include "logic/player.h"
1659+#include "logic/widelands.h"
1660 #include "ui_basic/button.h"
1661-#include "ui_basic/checkbox.h"
1662 #include "ui_basic/dropdown.h"
1663-#include "ui_basic/icon.h"
1664+#include "ui_basic/mouse_constants.h"
1665 #include "ui_basic/scrollbar.h"
1666 #include "ui_basic/textarea.h"
1667
1668+#define AI_NAME_PREFIX "ai" AI_NAME_SEPARATOR
1669+
1670+constexpr int kPadding = 4;
1671+
1672+/// Holds the info and dropdown menu for a connected client
1673 struct MultiPlayerClientGroup : public UI::Box {
1674 MultiPlayerClientGroup(UI::Panel* const parent,
1675- uint8_t id,
1676- int32_t const /* x */,
1677- int32_t const /* y */,
1678 int32_t const w,
1679 int32_t const h,
1680+ PlayerSlot id,
1681 GameSettingsProvider* const settings)
1682- : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h),
1683- type_icon(nullptr),
1684- type(nullptr),
1685+ : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h, kPadding),
1686+ slot_dropdown_(this, 0, 0, h, 200, h, _("Role"), UI::DropdownType::kPictorial),
1687+ // Name needs to be initialized after the dropdown, otherwise the layout function will
1688+ // crash.
1689+ name(this, 0, 0, w - h - UI::Scrollbar::kSize * 11 / 5, h),
1690 s(settings),
1691 id_(id),
1692- save_(-2) {
1693+ slot_selection_locked_(false) {
1694 set_size(w, h);
1695- name = new UI::Textarea(this, 0, 0, w - h - UI::Scrollbar::kSize * 11 / 5, h);
1696- add(name);
1697- // Either Button if changeable OR text if not
1698- if (id == settings->settings().usernum) { // Our Client
1699- type = new UI::Button(
1700- this, "client_type", 0, 0, h, h, g_gr->images().get("images/ui_basic/but1.png"), "");
1701- type->sigclicked.connect(
1702- boost::bind(&MultiPlayerClientGroup::toggle_type, boost::ref(*this)));
1703- add(type);
1704- } else { // just a shown client
1705- type_icon = new UI::Icon(
1706- this, 0, 0, h, h, g_gr->images().get("images/wui/fieldaction/menu_tab_watch.png"));
1707- add(type_icon);
1708- }
1709- }
1710-
1711- /// Switch human players and spectator
1712- void toggle_type() {
1713- UserSettings us = s->settings().users.at(id_);
1714- int16_t p = us.position;
1715- if (p == UserSettings::none())
1716- p = -1;
1717-
1718- for (++p; p < static_cast<int16_t>(s->settings().players.size()); ++p) {
1719- if (s->settings().players.at(p).state == PlayerSettings::stateHuman ||
1720- s->settings().players.at(p).state == PlayerSettings::stateOpen) {
1721- s->set_player_number(p);
1722- return;
1723- }
1724- }
1725- s->set_player_number(UserSettings::none());
1726- }
1727-
1728- /// Care about visibility and current values
1729- void refresh() {
1730- UserSettings us = s->settings().users.at(id_);
1731- if (us.position == UserSettings::not_connected()) {
1732- name->set_text((boost::format("<%s>") % _("free")).str());
1733- if (type)
1734- type->set_visible(false);
1735- else
1736- type_icon->set_visible(false);
1737- } else {
1738- name->set_text(us.name);
1739- if (save_ != us.position) {
1740- const Image* position_image;
1741- std::string temp_tooltip;
1742- if (us.position < UserSettings::highest_playernum()) {
1743- position_image =
1744- playercolor_image(us.position, "images/players/genstats_player.png");
1745- temp_tooltip =
1746- (boost::format(_("Player %u")) % static_cast<unsigned int>(us.position + 1))
1747- .str();
1748- } else {
1749- position_image = g_gr->images().get("images/wui/fieldaction/menu_tab_watch.png");
1750- temp_tooltip = _("Spectator");
1751- }
1752-
1753- // Either Button if changeable OR text if not
1754- if (id_ == s->settings().usernum) {
1755- type->set_pic(position_image);
1756- type->set_tooltip(temp_tooltip);
1757- type->set_visible(true);
1758- } else {
1759- type_icon->set_icon(position_image);
1760- type_icon->set_tooltip(temp_tooltip);
1761- type_icon->set_visible(true);
1762- }
1763- save_ = us.position;
1764- }
1765- }
1766- }
1767-
1768- UI::Textarea* name;
1769- UI::Icon* type_icon;
1770- UI::Button* type;
1771+
1772+ add(&slot_dropdown_);
1773+ add(&name, UI::Box::Resizing::kAlign, UI::Align::kCenter);
1774+
1775+ slot_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat);
1776+ slot_dropdown_.selected.connect(
1777+ boost::bind(&MultiPlayerClientGroup::set_slot, boost::ref(*this)));
1778+
1779+ update();
1780+ layout();
1781+
1782+ subscriber_ =
1783+ Notifications::subscribe<NoteGameSettings>([this](const NoteGameSettings& note) {
1784+ switch (note.action) {
1785+ case NoteGameSettings::Action::kMap:
1786+ /// In case the client gets kicked off its slot due to number of player slots in the
1787+ /// map
1788+ update();
1789+ break;
1790+ case NoteGameSettings::Action::kUser:
1791+ /// Player slot might have been closed, bumping the client to observer status. Also,
1792+ /// take note if another player changed their position.
1793+ if (id_ == note.usernum || note.usernum == UserSettings::none()) {
1794+ update();
1795+ }
1796+ break;
1797+ case NoteGameSettings::Action::kPlayer:
1798+ break;
1799+ }
1800+ });
1801+ }
1802+
1803+ /// Update dropdown sizes
1804+ void layout() override {
1805+ UI::Box::layout();
1806+ slot_dropdown_.set_height(g_gr->get_yres() * 3 / 4);
1807+ }
1808+
1809+ /// This will update the client's player slot with the value currently selected in the slot
1810+ /// dropdown.
1811+ void set_slot() {
1812+ const GameSettings& settings = s->settings();
1813+ if (id_ != settings.usernum) {
1814+ return;
1815+ }
1816+ slot_selection_locked_ = true;
1817+ if (slot_dropdown_.has_selection()) {
1818+ const uint8_t new_slot = slot_dropdown_.get_selected();
1819+ if (new_slot != settings.users.at(id_).position) {
1820+ s->set_player_number(slot_dropdown_.get_selected());
1821+ }
1822+ }
1823+ slot_selection_locked_ = false;
1824+ }
1825+
1826+ /// Rebuild the slot dropdown from the server settings. This will keep the host and client UIs in
1827+ /// sync.
1828+ void rebuild_slot_dropdown(const GameSettings& settings) {
1829+ if (slot_selection_locked_) {
1830+ return;
1831+ }
1832+ const UserSettings& user_setting = settings.users.at(id_);
1833+
1834+ slot_dropdown_.clear();
1835+ for (PlayerSlot slot = 0; slot < settings.players.size(); ++slot) {
1836+ if (settings.players.at(slot).state == PlayerSettings::State::kHuman ||
1837+ settings.players.at(slot).state == PlayerSettings::State::kOpen) {
1838+ slot_dropdown_.add((boost::format(_("Player %u")) % static_cast<unsigned int>(slot + 1)).str(),
1839+ slot, playercolor_image(slot, "images/players/genstats_player.png"),
1840+ slot == user_setting.position);
1841+ }
1842+ }
1843+ slot_dropdown_.add(_("Spectator"), UserSettings::none(),
1844+ g_gr->images().get("images/wui/fieldaction/menu_tab_watch.png"),
1845+ user_setting.position == UserSettings::none());
1846+ slot_dropdown_.set_visible(true);
1847+ slot_dropdown_.set_enabled(id_ == settings.usernum);
1848+ }
1849+
1850+ /// Take care of visibility and current values
1851+ void update() {
1852+ const GameSettings& settings = s->settings();
1853+ const UserSettings& user_setting = settings.users.at(id_);
1854+
1855+ if (user_setting.position == UserSettings::not_connected()) {
1856+ set_visible(false);
1857+ return;
1858+ }
1859+
1860+ name.set_text(user_setting.name);
1861+ rebuild_slot_dropdown(settings);
1862+ }
1863+
1864+ UI::Dropdown<uintptr_t> slot_dropdown_; /// Select the player slot.
1865+ UI::Textarea name; /// Client nick name
1866 GameSettingsProvider* const s;
1867- uint8_t const id_;
1868- int16_t save_; // saved position to check rewrite need.
1869+ uint8_t const id_; /// User number
1870+ bool slot_selection_locked_; // Ensure that dropdowns will close on selection.
1871+ std::unique_ptr<Notifications::Subscriber<NoteGameSettings>> subscriber_;
1872 };
1873
1874+/// Holds the dropdown menus for a player slot
1875 struct MultiPlayerPlayerGroup : public UI::Box {
1876 MultiPlayerPlayerGroup(UI::Panel* const parent,
1877- uint8_t id,
1878- int32_t const /* x */,
1879- int32_t const /* y */,
1880 int32_t const w,
1881 int32_t const h,
1882+ PlayerSlot id,
1883 GameSettingsProvider* const settings,
1884 NetworkPlayerSettingsBackend* const npsb)
1885- : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h),
1886- player(nullptr),
1887- type(nullptr),
1888- init(nullptr),
1889+ : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h, kPadding / 2),
1890 s(settings),
1891 n(npsb),
1892 id_(id),
1893+ player(this,
1894+ "player",
1895+ 0,
1896+ 0,
1897+ h,
1898+ h,
1899+ g_gr->images().get("images/ui_basic/but1.png"),
1900+ playercolor_image(id, "images/players/player_position_menu.png"),
1901+ (boost::format(_("Player %u")) % static_cast<unsigned int>(id_ + 1)).str(),
1902+ UI::Button::Style::kFlat),
1903+ type_dropdown_(this, 0, 0, 50, 200, h, _("Type"), UI::DropdownType::kPictorial),
1904 tribes_dropdown_(this, 0, 0, 50, 200, h, _("Tribe"), UI::DropdownType::kPictorial),
1905- last_state_(PlayerSettings::stateClosed),
1906- last_player_amount_(0) {
1907+ init_dropdown_(
1908+ this, 0, 0, w - 4 * h - 3 * kPadding, 200, h, "", UI::DropdownType::kTextualNarrow),
1909+ team_dropdown_(this, 0, 0, h, 200, h, _("Team"), UI::DropdownType::kPictorial),
1910+ last_state_(PlayerSettings::State::kClosed),
1911+ type_selection_locked_(false),
1912+ tribe_selection_locked_(false),
1913+ init_selection_locked_(false),
1914+ team_selection_locked_(false) {
1915 set_size(w, h);
1916- tribes_dropdown_.set_visible(false);
1917- tribes_dropdown_.set_enabled(false);
1918+
1919+ player.set_disable_style(UI::ButtonDisableStyle::kFlat);
1920+ player.set_enabled(false);
1921+
1922+ type_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat);
1923+ tribes_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat);
1924+ init_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat);
1925+ team_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat);
1926+
1927+ type_dropdown_.selected.connect(
1928+ boost::bind(&MultiPlayerPlayerGroup::set_type, boost::ref(*this)));
1929 tribes_dropdown_.selected.connect(
1930 boost::bind(&MultiPlayerPlayerGroup::set_tribe_or_shared_in, boost::ref(*this)));
1931+ init_dropdown_.selected.connect(
1932+ boost::bind(&MultiPlayerPlayerGroup::set_init, boost::ref(*this)));
1933+ team_dropdown_.selected.connect(
1934+ boost::bind(&MultiPlayerPlayerGroup::set_team, boost::ref(*this)));
1935
1936- const Image* player_image = playercolor_image(id, "images/players/player_position_menu.png");
1937- assert(player_image);
1938- player = new UI::Icon(this, 0, 0, h, h, player_image);
1939- add(player);
1940- type = new UI::Button(
1941- this, "player_type", 0, 0, h, h, g_gr->images().get("images/ui_basic/but1.png"), "");
1942- type->sigclicked.connect(
1943- boost::bind(&MultiPlayerPlayerGroup::toggle_type, boost::ref(*this)));
1944- add(type);
1945+ add_space(0);
1946+ add(&player);
1947+ add(&type_dropdown_);
1948 add(&tribes_dropdown_);
1949- init = new UI::Button(this, "player_init", 0, 0, w - 4 * h, h,
1950- g_gr->images().get("images/ui_basic/but1.png"), "");
1951- init->sigclicked.connect(
1952- boost::bind(&MultiPlayerPlayerGroup::toggle_init, boost::ref(*this)));
1953- add(init);
1954- team = new UI::Button(
1955- this, "player_team", 0, 0, h, h, g_gr->images().get("images/ui_basic/but1.png"), "");
1956- team->sigclicked.connect(
1957- boost::bind(&MultiPlayerPlayerGroup::toggle_team, boost::ref(*this)));
1958- add(team);
1959- }
1960-
1961- /// Toggle through the types
1962- void toggle_type() {
1963- n->toggle_type(id_);
1964+ add(&init_dropdown_);
1965+ add(&team_dropdown_);
1966+ add_space(0);
1967+
1968+ subscriber_ =
1969+ Notifications::subscribe<NoteGameSettings>([this](const NoteGameSettings& note) {
1970+ switch (note.action) {
1971+ case NoteGameSettings::Action::kMap:
1972+ // We don't care about map updates, since we receive enough notifications for the
1973+ // slots.
1974+ break;
1975+ default:
1976+ if (s->settings().players.empty()) {
1977+ // No map/savegame yet
1978+ return;
1979+ }
1980+ if (id_ == note.position ||
1981+ s->settings().players[id_].state == PlayerSettings::State::kShared) {
1982+ update();
1983+ }
1984+ }
1985+ });
1986+
1987+ // Init dropdowns
1988+ update();
1989+ layout();
1990+ }
1991+
1992+ /// Update dropdown sizes
1993+ void layout() override {
1994+ const int max_height = g_gr->get_yres() * 3 / 4;
1995+ type_dropdown_.set_height(max_height);
1996+ tribes_dropdown_.set_height(max_height);
1997+ init_dropdown_.set_height(max_height);
1998+ team_dropdown_.set_height(max_height);
1999+ UI::Box::layout();
2000+ }
2001+
2002+ /// This will update the game settings for the type with the value
2003+ /// currently selected in the type dropdown.
2004+ void set_type() {
2005+ if (!s->can_change_player_state(id_)) {
2006+ return;
2007+ }
2008+ type_selection_locked_ = true;
2009+ if (type_dropdown_.has_selection()) {
2010+ const std::string& selected = type_dropdown_.get_selected();
2011+ PlayerSettings::State state = PlayerSettings::State::kComputer;
2012+ if (selected == "closed") {
2013+ state = PlayerSettings::State::kClosed;
2014+ } else if (selected == "open") {
2015+ state = PlayerSettings::State::kOpen;
2016+ } else if (selected == "shared_in") {
2017+ state = PlayerSettings::State::kShared;
2018+ } else {
2019+ if (selected == AI_NAME_PREFIX "random") {
2020+ n->set_player_ai(id_, "", true);
2021+ } else {
2022+ if (boost::starts_with(selected, AI_NAME_PREFIX)) {
2023+ std::vector<std::string> parts;
2024+ boost::split(parts, selected, boost::is_any_of(AI_NAME_SEPARATOR));
2025+ assert(parts.size() == 2);
2026+ n->set_player_ai(id_, parts[1], false);
2027+ } else {
2028+ throw wexception("Unknown player state: %s\n", selected.c_str());
2029+ }
2030+ }
2031+ }
2032+ n->set_player_state(id_, state);
2033+ }
2034+ type_selection_locked_ = false;
2035+ }
2036+
2037+ /// Rebuild the type dropdown from the server settings. This will keep the host and client UIs in
2038+ /// sync.
2039+ void rebuild_type_dropdown(const GameSettings& settings) {
2040+ if (type_selection_locked_) {
2041+ return;
2042+ }
2043+ type_dropdown_.clear();
2044+ // AIs
2045+ for (const auto* impl : ComputerPlayer::get_implementations()) {
2046+ type_dropdown_.add(_(impl->descname),
2047+ (boost::format(AI_NAME_PREFIX "%s") % impl->name).str(),
2048+ g_gr->images().get(impl->icon_filename), false, _(impl->descname));
2049+ }
2050+ /** TRANSLATORS: This is the name of an AI used in the game setup screens */
2051+ type_dropdown_.add(_("Random AI"), AI_NAME_PREFIX "random",
2052+ g_gr->images().get("images/ai/ai_random.png"), false, _("Random AI"));
2053+
2054+ // Slot state. Only add shared_in if there are viable slots
2055+ if (settings.is_shared_usable(id_, settings.find_shared(id_))) {
2056+ type_dropdown_.add(_("Shared in"), "shared_in",
2057+ g_gr->images().get("images/ui_fsmenu/shared_in.png"), false,
2058+ _("Shared in"));
2059+ }
2060+
2061+ // Do not close a player in savegames or scenarios
2062+ if (!settings.uncloseable(id_)) {
2063+ type_dropdown_.add(_("Closed"), "closed", g_gr->images().get("images/ui_basic/stop.png"),
2064+ false, _("Closed"));
2065+ }
2066+
2067+ type_dropdown_.add(
2068+ _("Open"), "open", g_gr->images().get("images/ui_basic/continue.png"), false, _("Open"));
2069+
2070+ type_dropdown_.set_enabled(s->can_change_player_state(id_));
2071+
2072+ // Now select the entry according to server settings
2073+ const PlayerSettings& player_setting = settings.players[id_];
2074+ if (player_setting.state == PlayerSettings::State::kHuman) {
2075+ type_dropdown_.set_image(g_gr->images().get("images/wui/stats/genstats_nrworkers.png"));
2076+ type_dropdown_.set_tooltip((boost::format(_("%1%: %2%")) % _("Type") % _("Human")).str());
2077+ } else if (player_setting.state == PlayerSettings::State::kClosed) {
2078+ type_dropdown_.select("closed");
2079+ } else if (player_setting.state == PlayerSettings::State::kOpen) {
2080+ type_dropdown_.select("open");
2081+ } else if (player_setting.state == PlayerSettings::State::kShared) {
2082+ type_dropdown_.select("shared_in");
2083+ } else {
2084+ if (player_setting.state == PlayerSettings::State::kComputer) {
2085+ if (player_setting.ai.empty()) {
2086+ type_dropdown_.set_errored(_("No AI"));
2087+ } else {
2088+ if (player_setting.random_ai) {
2089+ type_dropdown_.select(AI_NAME_PREFIX "random");
2090+ } else {
2091+ const ComputerPlayer::Implementation* impl =
2092+ ComputerPlayer::get_implementation(player_setting.ai);
2093+ type_dropdown_.select((boost::format(AI_NAME_PREFIX "%s") % impl->name).str());
2094+ }
2095+ }
2096+ }
2097+ }
2098+ }
2099+
2100+ /// Whether the client who is running the UI is allowed to change the tribe for this player slot.
2101+ bool has_tribe_access() {
2102+ return s->settings().players[id_].state == PlayerSettings::State::kShared ?
2103+ s->can_change_player_init(id_) :
2104+ s->can_change_player_tribe(id_);
2105 }
2106
2107 /// This will update the game settings for the tribe or shared_in with the value
2108 /// currently selected in the tribes dropdown.
2109 void set_tribe_or_shared_in() {
2110- n->set_block_tribe_selection(true);
2111- tribes_dropdown_.set_disable_style(s->settings().players[id_].state ==
2112- PlayerSettings::stateShared ?
2113+ if (!has_tribe_access()) {
2114+ return;
2115+ }
2116+ const PlayerSettings& player_settings = s->settings().players[id_];
2117+ tribe_selection_locked_ = true;
2118+ tribes_dropdown_.set_disable_style(player_settings.state == PlayerSettings::State::kShared ?
2119 UI::ButtonDisableStyle::kPermpressed :
2120- UI::ButtonDisableStyle::kMonochrome);
2121+ UI::ButtonDisableStyle::kFlat);
2122 if (tribes_dropdown_.has_selection()) {
2123- if (s->settings().players[id_].state == PlayerSettings::stateShared) {
2124- n->set_shared_in(
2125+ if (player_settings.state == PlayerSettings::State::kShared) {
2126+ n->set_player_shared(
2127 id_, boost::lexical_cast<unsigned int>(tribes_dropdown_.get_selected()));
2128 } else {
2129- n->set_tribe(id_, tribes_dropdown_.get_selected());
2130- }
2131- }
2132- n->set_block_tribe_selection(false);
2133- }
2134-
2135- /// Toggle through the initializations
2136- void toggle_init() {
2137- n->toggle_init(id_);
2138- }
2139-
2140- /// Toggle through the teams
2141- void toggle_team() {
2142- n->toggle_team(id_);
2143- }
2144-
2145- /// Helper function to cast shared_in for use in the dropdown.
2146- const std::string shared_in_as_string(uint8_t shared_in) {
2147- return boost::lexical_cast<std::string>(static_cast<unsigned int>(shared_in));
2148- }
2149-
2150- /// Update the tribes dropdown from the server settings if the server setting changed.
2151- /// This will keep the host and client UIs in sync.
2152- void update_tribes_dropdown(const PlayerSettings& player_setting) {
2153- if (player_setting.state == PlayerSettings::stateClosed ||
2154- player_setting.state == PlayerSettings::stateOpen) {
2155+ n->set_player_tribe(id_, tribes_dropdown_.get_selected());
2156+ }
2157+ }
2158+ tribe_selection_locked_ = false;
2159+ }
2160+
2161+ /// Rebuild the tribes dropdown from the server settings. This will keep the host and client UIs
2162+ /// in sync.
2163+ void rebuild_tribes_dropdown(const GameSettings& settings) {
2164+ if (tribe_selection_locked_) {
2165+ return;
2166+ }
2167+ const PlayerSettings& player_setting = settings.players[id_];
2168+ tribes_dropdown_.clear();
2169+ if (player_setting.state == PlayerSettings::State::kShared) {
2170+ for (size_t i = 0; i < settings.players.size(); ++i) {
2171+ if (i != id_) {
2172+ // Do not add players that are also shared_in or closed.
2173+ const PlayerSettings& other_setting = settings.players[i];
2174+ if (!PlayerSettings::can_be_shared(other_setting.state)) {
2175+ continue;
2176+ }
2177+
2178+ const Image* player_image =
2179+ playercolor_image(i, "images/players/player_position_menu.png");
2180+ assert(player_image);
2181+ const std::string player_name =
2182+ /** TRANSLATORS: This is an option in multiplayer setup for sharing
2183+ another player's starting position. */
2184+ (boost::format(_("Shared in Player %u")) % static_cast<unsigned int>(i + 1)).str();
2185+ tribes_dropdown_.add(player_name,
2186+ boost::lexical_cast<std::string>(static_cast<unsigned int>(i + 1)),
2187+ player_image, (i + 1) == player_setting.shared_in, player_name);
2188+ }
2189+ }
2190+ tribes_dropdown_.set_enabled(tribes_dropdown_.size() > 1);
2191+ } else {
2192+ {
2193+ i18n::Textdomain td("tribes");
2194+ for (const TribeBasicInfo& tribeinfo : Widelands::get_all_tribeinfos()) {
2195+ tribes_dropdown_.add(_(tribeinfo.descname), tribeinfo.name,
2196+ g_gr->images().get(tribeinfo.icon), false, tribeinfo.tooltip);
2197+ }
2198+ }
2199+ tribes_dropdown_.add(pgettext("tribe", "Random"), "random",
2200+ g_gr->images().get("images/ui_fsmenu/random.png"), false,
2201+ _("The tribe will be selected at random"));
2202+ if (player_setting.random_tribe) {
2203+ tribes_dropdown_.select("random");
2204+ } else {
2205+ tribes_dropdown_.select(player_setting.tribe);
2206+ }
2207+ }
2208+ const bool has_access = has_tribe_access();
2209+ if (tribes_dropdown_.is_enabled() != has_access) {
2210+ tribes_dropdown_.set_enabled(has_access && tribes_dropdown_.size() > 1);
2211+ }
2212+ if (player_setting.state == PlayerSettings::State::kClosed ||
2213+ player_setting.state == PlayerSettings::State::kOpen) {
2214 return;
2215 }
2216 if (!tribes_dropdown_.is_visible()) {
2217 tribes_dropdown_.set_visible(true);
2218 }
2219- if (!tribes_dropdown_.is_expanded() && !n->tribe_selection_blocked &&
2220- tribes_dropdown_.has_selection()) {
2221- const std::string selected_tribe = tribes_dropdown_.get_selected();
2222- if (player_setting.state == PlayerSettings::stateShared) {
2223- const std::string shared_in = shared_in_as_string(player_setting.shared_in);
2224- if (shared_in != selected_tribe) {
2225- tribes_dropdown_.select(shared_in);
2226- }
2227- } else {
2228- if (player_setting.random_tribe) {
2229- if (selected_tribe != "random") {
2230- tribes_dropdown_.select("random");
2231- }
2232- } else if (selected_tribe != player_setting.tribe) {
2233- tribes_dropdown_.select(player_setting.tribe);
2234- }
2235- }
2236- }
2237- }
2238-
2239- /// If the map was changed or the selection mode changed between shared_in and tribe, rebuild the
2240- /// dropdown.
2241- void rebuild_tribes_dropdown(const GameSettings& settings) {
2242- const PlayerSettings& player_setting = settings.players[id_];
2243-
2244- if (player_setting.state == PlayerSettings::stateClosed ||
2245- player_setting.state == PlayerSettings::stateOpen) {
2246- return;
2247- }
2248-
2249- if (tribes_dropdown_.empty() || last_player_amount_ != settings.players.size() ||
2250- ((player_setting.state == PlayerSettings::stateShared ||
2251- last_state_ == PlayerSettings::stateShared) &&
2252- player_setting.state != last_state_)) {
2253- tribes_dropdown_.clear();
2254-
2255- // We need to see the playercolor if setting shared_in is disabled
2256- tribes_dropdown_.set_disable_style(player_setting.state == PlayerSettings::stateShared ?
2257- UI::ButtonDisableStyle::kPermpressed :
2258- UI::ButtonDisableStyle::kMonochrome);
2259-
2260- if (player_setting.state == PlayerSettings::stateShared) {
2261- for (size_t i = 0; i < settings.players.size(); ++i) {
2262- if (i != id_) {
2263- // TODO(GunChleoc): Do not add players that are also shared_in.
2264- const Image* player_image =
2265- playercolor_image(i, "images/players/player_position_menu.png");
2266- assert(player_image);
2267- const std::string player_name =
2268- /** TRANSLATORS: This is an option in multiplayer setup for sharing
2269- another player's starting position. */
2270- (boost::format(_("Shared in Player %u")) % static_cast<unsigned int>(i + 1))
2271- .str();
2272- tribes_dropdown_.add(
2273- player_name, shared_in_as_string(i + 1), player_image, false, player_name);
2274- }
2275- }
2276- int shared_in = 0;
2277- while (shared_in == id_) {
2278- ++shared_in;
2279- }
2280- tribes_dropdown_.select(shared_in_as_string(shared_in + 1));
2281- tribes_dropdown_.set_enabled(tribes_dropdown_.size() > 1);
2282- } else {
2283- {
2284- i18n::Textdomain td("tribes");
2285- for (const TribeBasicInfo& tribeinfo : Widelands::get_all_tribeinfos()) {
2286- tribes_dropdown_.add(_(tribeinfo.descname), tribeinfo.name,
2287- g_gr->images().get(tribeinfo.icon), false,
2288- tribeinfo.tooltip);
2289- }
2290- }
2291- tribes_dropdown_.add(pgettext("tribe", "Random"), "random",
2292- g_gr->images().get("images/ui_fsmenu/random.png"), false,
2293- _("The tribe will be selected at random"));
2294- if (player_setting.random_tribe) {
2295- tribes_dropdown_.select("random");
2296- } else {
2297- tribes_dropdown_.select(player_setting.tribe);
2298- }
2299- }
2300- }
2301- last_player_amount_ = settings.players.size();
2302+ }
2303+
2304+ /// This will update the game settings for the initialization with the value
2305+ /// currently selected in the initialization dropdown.
2306+ void set_init() {
2307+ if (!s->can_change_player_init(id_)) {
2308+ return;
2309+ }
2310+ init_selection_locked_ = true;
2311+ if (init_dropdown_.has_selection()) {
2312+ n->set_player_init(id_, init_dropdown_.get_selected());
2313+ }
2314+ init_selection_locked_ = false;
2315+ }
2316+
2317+ /// Rebuild the init dropdown from the server settings. This will keep the host and client UIs in
2318+ /// sync.
2319+ void rebuild_init_dropdown(const GameSettings& settings) {
2320+ if (init_selection_locked_) {
2321+ return;
2322+ }
2323+
2324+ init_dropdown_.clear();
2325+ const PlayerSettings& player_setting = settings.players[id_];
2326+ if (settings.scenario) {
2327+ init_dropdown_.set_label(_("Scenario"));
2328+ } else if (settings.savegame) {
2329+ /** Translators: This is a game type */
2330+ init_dropdown_.set_label(_("Saved Game"));
2331+ } else {
2332+ init_dropdown_.set_label("");
2333+ i18n::Textdomain td("tribes"); // for translated initialisation
2334+ const TribeBasicInfo tribeinfo = Widelands::get_tribeinfo(player_setting.tribe);
2335+ for (size_t i = 0; i < tribeinfo.initializations.size(); ++i) {
2336+ const TribeBasicInfo::Initialization& addme = tribeinfo.initializations[i];
2337+ init_dropdown_.add(_(addme.descname), i, nullptr,
2338+ i == player_setting.initialization_index, _(addme.tooltip));
2339+ }
2340+ }
2341+
2342+ init_dropdown_.set_visible(true);
2343+ init_dropdown_.set_enabled(s->can_change_player_init(id_));
2344+ }
2345+
2346+ /// This will update the team settings with the value currently selected in the teams dropdown.
2347+ void set_team() {
2348+ team_selection_locked_ = true;
2349+ if (team_dropdown_.has_selection()) {
2350+ n->set_player_team(id_, team_dropdown_.get_selected());
2351+ }
2352+ team_selection_locked_ = false;
2353+ }
2354+
2355+ /// Rebuild the team dropdown from the server settings. This will keep the host and client UIs in
2356+ /// sync.
2357+ void rebuild_team_dropdown(const GameSettings& settings) {
2358+ if (team_selection_locked_) {
2359+ return;
2360+ }
2361+ const PlayerSettings& player_setting = settings.players[id_];
2362+ if (player_setting.state == PlayerSettings::State::kShared) {
2363+ team_dropdown_.set_visible(false);
2364+ team_dropdown_.set_enabled(false);
2365+ return;
2366+ }
2367+
2368+ team_dropdown_.clear();
2369+ team_dropdown_.add(_("No Team"), 0, g_gr->images().get("images/players/no_team.png"));
2370+#ifndef NDEBUG
2371+ const size_t no_of_team_colors = sizeof(kTeamColors) / sizeof(kTeamColors[0]);
2372+#endif
2373+ for (Widelands::TeamNumber t = 1; t <= settings.players.size() / 2; ++t) {
2374+ assert(t < no_of_team_colors);
2375+ team_dropdown_.add((boost::format(_("Team %d")) % static_cast<unsigned int>(t)).str(), t,
2376+ playercolor_image(kTeamColors[t], "images/players/team.png"));
2377+ }
2378+ team_dropdown_.select(player_setting.team);
2379+ team_dropdown_.set_visible(true);
2380+ team_dropdown_.set_enabled(s->can_change_player_team(id_));
2381 }
2382
2383 /// Refresh all user interfaces
2384- void refresh() {
2385+ void update() {
2386 const GameSettings& settings = s->settings();
2387-
2388 if (id_ >= settings.players.size()) {
2389 set_visible(false);
2390 return;
2391 }
2392
2393- n->refresh(id_);
2394-
2395+ const PlayerSettings& player_setting = settings.players[id_];
2396+ rebuild_type_dropdown(settings);
2397 set_visible(true);
2398
2399- const PlayerSettings& player_setting = settings.players[id_];
2400- bool typeaccess = s->can_change_player_state(id_);
2401- bool tribeaccess = s->can_change_player_tribe(id_);
2402- bool const initaccess = s->can_change_player_init(id_);
2403- bool teamaccess = s->can_change_player_team(id_);
2404- type->set_enabled(typeaccess);
2405-
2406- rebuild_tribes_dropdown(settings);
2407-
2408- if (player_setting.state == PlayerSettings::stateClosed) {
2409- type->set_tooltip(_("Closed"));
2410- type->set_pic(g_gr->images().get("images/ui_basic/stop.png"));
2411- team->set_visible(false);
2412- team->set_enabled(false);
2413- tribes_dropdown_.set_visible(false);
2414- tribes_dropdown_.set_enabled(false);
2415- init->set_visible(false);
2416- init->set_enabled(false);
2417- return;
2418- } else if (player_setting.state == PlayerSettings::stateOpen) {
2419- type->set_tooltip(_("Open"));
2420- type->set_pic(g_gr->images().get("images/ui_basic/continue.png"));
2421- team->set_visible(false);
2422- team->set_enabled(false);
2423- tribes_dropdown_.set_visible(false);
2424- tribes_dropdown_.set_enabled(false);
2425- init->set_visible(false);
2426- init->set_enabled(false);
2427- return;
2428- } else if (player_setting.state == PlayerSettings::stateShared) {
2429- type->set_tooltip(_("Shared in"));
2430- type->set_pic(g_gr->images().get("images/ui_fsmenu/shared_in.png"));
2431-
2432- update_tribes_dropdown(player_setting);
2433-
2434- if (tribes_dropdown_.is_enabled() != initaccess) {
2435- tribes_dropdown_.set_enabled(initaccess && !n->tribe_selection_blocked &&
2436- tribes_dropdown_.size() > 1);
2437- }
2438-
2439- team->set_visible(false);
2440- team->set_enabled(false);
2441-
2442+ if (player_setting.state == PlayerSettings::State::kClosed ||
2443+ player_setting.state == PlayerSettings::State::kOpen) {
2444+ team_dropdown_.set_visible(false);
2445+ team_dropdown_.set_enabled(false);
2446+ tribes_dropdown_.set_visible(false);
2447+ tribes_dropdown_.set_enabled(false);
2448+ init_dropdown_.set_visible(false);
2449+ init_dropdown_.set_enabled(false);
2450 } else {
2451- std::string title;
2452- std::string pic = "images/";
2453- if (player_setting.state == PlayerSettings::stateComputer) {
2454- if (player_setting.ai.empty()) {
2455- title = _("Computer");
2456- pic += "novalue.png";
2457- } else {
2458- if (player_setting.random_ai) {
2459- /** TRANSLATORS: This is the name of an AI used in the game setup screens */
2460- title = _("Random AI");
2461- pic += "ai/ai_random.png";
2462- } else {
2463- const ComputerPlayer::Implementation* impl =
2464- ComputerPlayer::get_implementation(player_setting.ai);
2465- title = _(impl->descname);
2466- pic = impl->icon_filename;
2467- }
2468- }
2469- } else { // PlayerSettings::stateHuman
2470- title = _("Human");
2471- pic += "wui/stats/genstats_nrworkers.png";
2472- }
2473- type->set_tooltip(title.c_str());
2474- type->set_pic(g_gr->images().get(pic));
2475-
2476- update_tribes_dropdown(player_setting);
2477-
2478- if (tribes_dropdown_.is_enabled() != tribeaccess) {
2479- tribes_dropdown_.set_enabled(tribeaccess && !n->tribe_selection_blocked);
2480- }
2481-
2482- if (player_setting.team) {
2483- team->set_title(std::to_string(static_cast<unsigned int>(player_setting.team)));
2484- } else {
2485- team->set_title("--");
2486- }
2487- team->set_visible(true);
2488- team->set_enabled(teamaccess);
2489- }
2490- init->set_enabled(initaccess);
2491- init->set_visible(true);
2492-
2493- if (settings.scenario)
2494- init->set_title(_("Scenario"));
2495- else if (settings.savegame)
2496- /** Translators: This is a game type */
2497- init->set_title(_("Saved Game"));
2498- else {
2499- i18n::Textdomain td("tribes"); // for translated initialisation
2500- for (const TribeBasicInfo& tribeinfo : settings.tribes) {
2501- if (tribeinfo.name == player_setting.tribe) {
2502- init->set_title(
2503- _(tribeinfo.initializations.at(player_setting.initialization_index).descname));
2504- init->set_tooltip(
2505- _(tribeinfo.initializations.at(player_setting.initialization_index).tooltip));
2506- break;
2507- }
2508- }
2509- }
2510- last_state_ = player_setting.state;
2511+ rebuild_tribes_dropdown(settings);
2512+ rebuild_init_dropdown(settings);
2513+ rebuild_team_dropdown(settings);
2514+ }
2515+
2516+ // Trigger update for the other players for shared_in mode when slots open and close
2517+ if (last_state_ != player_setting.state) {
2518+ last_state_ = player_setting.state;
2519+ for (PlayerSlot slot = 0; slot < s->settings().players.size(); ++slot) {
2520+ if (slot != id_) {
2521+ n->set_player_state(slot, settings.players[slot].state);
2522+ }
2523+ }
2524+ }
2525 }
2526
2527- UI::Icon* player;
2528- UI::Button* type;
2529- UI::Button* init;
2530- UI::Button* team;
2531 GameSettingsProvider* const s;
2532 NetworkPlayerSettingsBackend* const n;
2533- uint8_t const id_;
2534+ PlayerSlot const id_;
2535+
2536+ UI::Button player;
2537+ UI::Dropdown<std::string>
2538+ type_dropdown_; /// Select who owns the slot (human, AI, open, closed, shared-in).
2539 UI::Dropdown<std::string> tribes_dropdown_; /// Select the tribe or shared_in player.
2540- PlayerSettings::State last_state_; /// The dropdown needs updating if this changes
2541- size_t last_player_amount_; /// The dropdown needs rebuilding if this changes
2542+ UI::Dropdown<uintptr_t>
2543+ init_dropdown_; /// Select the initialization (Headquarters, Fortified Village etc.)
2544+ UI::Dropdown<uintptr_t> team_dropdown_; /// Select the team number
2545+ PlayerSettings::State
2546+ last_state_; /// The dropdowns for the other slots need updating if this changes
2547+ /// Lock rebuilding dropdowns so that they can close on selection
2548+ bool type_selection_locked_;
2549+ bool tribe_selection_locked_;
2550+ bool init_selection_locked_;
2551+ bool team_selection_locked_;
2552+
2553+ std::unique_ptr<Notifications::Subscriber<NoteGameSettings>> subscriber_;
2554 };
2555
2556 MultiPlayerSetupGroup::MultiPlayerSetupGroup(UI::Panel* const parent,
2557@@ -457,69 +590,41 @@
2558 int32_t const w,
2559 int32_t const h,
2560 GameSettingsProvider* const settings,
2561- uint32_t /* butw */,
2562 uint32_t buth)
2563- : UI::Panel(parent, x, y, w, h),
2564+ : UI::Box(parent, x, y, UI::Box::Horizontal, w, h, 8 * kPadding),
2565 s(settings),
2566 npsb(new NetworkPlayerSettingsBackend(s)),
2567- clientbox(this, 0, buth, UI::Box::Vertical, w / 3, h - buth),
2568- playerbox(this, w * 6 / 15, buth, UI::Box::Vertical, w * 9 / 15, h - buth),
2569+ clientbox(this, 0, 0, UI::Box::Vertical),
2570+ playerbox(this, 0, 0, UI::Box::Vertical, w * 9 / 15, h, kPadding),
2571 buth_(buth) {
2572- int small_font = UI_FONT_SIZE_SMALL * 3 / 4;
2573-
2574- // Clientbox and labels
2575- labels.push_back(new UI::Textarea(
2576- this, UI::Scrollbar::kSize * 6 / 5, buth / 3, w / 3 - buth - UI::Scrollbar::kSize * 2, buth));
2577- labels.back()->set_text(_("Client name"));
2578- labels.back()->set_fontsize(small_font);
2579-
2580- labels.push_back(new UI::Textarea(
2581- this, w / 3 - buth - UI::Scrollbar::kSize * 6 / 5, buth / 3, buth * 2, buth));
2582- labels.back()->set_text(_("Role"));
2583- labels.back()->set_fontsize(small_font);
2584-
2585- clientbox.set_size(w / 3, h - buth);
2586+ clientbox.set_size(w / 3, h);
2587 clientbox.set_scrolling(true);
2588
2589- // Playerbox and labels
2590- labels.push_back(new UI::Textarea(this, w * 6 / 15, buth / 3, buth, buth));
2591- labels.back()->set_text(_("Start"));
2592- labels.back()->set_fontsize(small_font);
2593-
2594- labels.push_back(new UI::Textarea(this, w * 6 / 15 + buth, buth / 3 - 10, buth, buth));
2595- labels.back()->set_text(_("Type"));
2596- labels.back()->set_fontsize(small_font);
2597-
2598- labels.push_back(new UI::Textarea(this, w * 6 / 15 + buth * 2, buth / 3, buth, buth));
2599- labels.back()->set_text(_("Tribe"));
2600- labels.back()->set_fontsize(small_font);
2601-
2602- labels.push_back(new UI::Textarea(
2603- this, w * 6 / 15 + buth * 3, buth / 3, w * 9 / 15 - 4 * buth, buth, UI::Align::kCenter));
2604- labels.back()->set_text(_("Initialization"));
2605- labels.back()->set_fontsize(small_font);
2606-
2607- labels.push_back(new UI::Textarea(this, w - buth, buth / 3, buth, buth, UI::Align::kRight));
2608- labels.back()->set_text(_("Team"));
2609- labels.back()->set_fontsize(small_font);
2610-
2611- playerbox.set_size(w * 9 / 15, h - buth);
2612+ add(&clientbox, UI::Box::Resizing::kExpandBoth);
2613+ add(&playerbox);
2614+
2615+ // Playerbox
2616+ playerbox.set_size(w * 9 / 15, h);
2617+ playerbox.add_space(0);
2618 multi_player_player_groups.resize(kMaxPlayers);
2619- for (uint8_t i = 0; i < multi_player_player_groups.size(); ++i) {
2620+ for (PlayerSlot i = 0; i < multi_player_player_groups.size(); ++i) {
2621 multi_player_player_groups.at(i) =
2622- new MultiPlayerPlayerGroup(&playerbox, i, 0, 0, playerbox.get_w(), buth, s, npsb.get());
2623+ new MultiPlayerPlayerGroup(&playerbox, playerbox.get_w(), buth_, i, s, npsb.get());
2624 playerbox.add(multi_player_player_groups.at(i));
2625 }
2626- refresh();
2627+ playerbox.add_space(0);
2628+
2629+ subscriber_ =
2630+ Notifications::subscribe<NoteGameSettings>([this](const NoteGameSettings&) { update(); });
2631+ set_size(w, h);
2632+ update();
2633 }
2634
2635 MultiPlayerSetupGroup::~MultiPlayerSetupGroup() {
2636 }
2637
2638-/**
2639- * Update display and enabled buttons based on current settings.
2640- */
2641-void MultiPlayerSetupGroup::refresh() {
2642+/// Update which slots are available based on current settings.
2643+void MultiPlayerSetupGroup::update() {
2644 const GameSettings& settings = s->settings();
2645
2646 // Update / initialize client groups
2647@@ -529,15 +634,29 @@
2648 for (uint32_t i = 0; i < settings.users.size(); ++i) {
2649 if (!multi_player_client_groups.at(i)) {
2650 multi_player_client_groups.at(i) =
2651- new MultiPlayerClientGroup(&clientbox, i, 0, 0, clientbox.get_w(), buth_, s);
2652- clientbox.add(
2653- &*multi_player_client_groups.at(i), UI::Box::Resizing::kAlign, UI::Align::kCenter);
2654- }
2655- multi_player_client_groups.at(i)->refresh();
2656- }
2657-
2658- // Update player groups
2659- for (uint32_t i = 0; i < kMaxPlayers; ++i) {
2660- multi_player_player_groups.at(i)->refresh();
2661+ new MultiPlayerClientGroup(&clientbox, clientbox.get_w(), buth_, i, s);
2662+ clientbox.add(multi_player_client_groups.at(i), UI::Box::Resizing::kFullSize);
2663+ multi_player_client_groups.at(i)->layout();
2664+ }
2665+ multi_player_client_groups.at(i)->set_visible(true);
2666+ }
2667+
2668+ // Keep track of which player slots are visible
2669+ for (PlayerSlot i = 0; i < multi_player_player_groups.size(); ++i) {
2670+ const bool should_be_visible = i < settings.players.size();
2671+ if (should_be_visible != multi_player_player_groups.at(i)->is_visible()) {
2672+ multi_player_player_groups.at(i)->set_visible(should_be_visible);
2673+ }
2674+ }
2675+}
2676+
2677+void MultiPlayerSetupGroup::draw(RenderTarget& dst) {
2678+ for (MultiPlayerPlayerGroup* player_group : multi_player_player_groups) {
2679+ if (player_group->is_visible()) {
2680+ dst.brighten_rect(
2681+ Recti(playerbox.get_x(), playerbox.get_y() + player_group->get_y() - kPadding / 2,
2682+ playerbox.get_w() + kPadding, player_group->get_h() + kPadding),
2683+ -MOUSE_OVER_BRIGHT_FACTOR);
2684+ }
2685 }
2686 }
2687
2688=== modified file 'src/wui/multiplayersetupgroup.h'
2689--- src/wui/multiplayersetupgroup.h 2017-01-25 18:55:59 +0000
2690+++ src/wui/multiplayersetupgroup.h 2017-09-02 13:55:49 +0000
2691@@ -33,7 +33,6 @@
2692 #include "ui_basic/textarea.h"
2693
2694 struct GameSettingsProvider;
2695-struct MultiPlayerSetupGroupOptions;
2696 struct MultiPlayerClientGroup;
2697 struct MultiPlayerPlayerGroup;
2698
2699@@ -44,26 +43,27 @@
2700 * clients, computers and closed players.
2701 *
2702 */
2703-struct MultiPlayerSetupGroup : public UI::Panel {
2704+struct MultiPlayerSetupGroup : public UI::Box {
2705 MultiPlayerSetupGroup(UI::Panel* parent,
2706 int32_t x,
2707 int32_t y,
2708 int32_t w,
2709 int32_t h,
2710 GameSettingsProvider* settings,
2711- uint32_t butw,
2712 uint32_t buth);
2713 ~MultiPlayerSetupGroup();
2714
2715- void refresh();
2716-
2717 private:
2718+ void update();
2719+ void draw(RenderTarget& dst) override;
2720+
2721 GameSettingsProvider* const s;
2722 std::unique_ptr<NetworkPlayerSettingsBackend> npsb;
2723 std::vector<MultiPlayerClientGroup*> multi_player_client_groups; // not owned
2724 std::vector<MultiPlayerPlayerGroup*> multi_player_player_groups; // not owned
2725+ std::unique_ptr<Notifications::Subscriber<NoteGameSettings>> subscriber_;
2726+
2727 UI::Box clientbox, playerbox;
2728- std::vector<UI::Textarea*> labels;
2729
2730 uint32_t buth_;
2731
2732
2733=== modified file 'src/wui/playerdescrgroup.cc'
2734--- src/wui/playerdescrgroup.cc 2017-01-30 14:40:12 +0000
2735+++ src/wui/playerdescrgroup.cc 2017-09-02 13:55:49 +0000
2736@@ -116,7 +116,7 @@
2737
2738 d->btnEnablePlayer->set_enabled(stateaccess);
2739
2740- if (player.state == PlayerSettings::stateClosed) {
2741+ if (player.state == PlayerSettings::State::kClosed) {
2742 d->btnEnablePlayer->set_state(false);
2743 d->btnPlayerTeam->set_visible(false);
2744 d->btnPlayerTeam->set_enabled(false);
2745@@ -132,7 +132,7 @@
2746 d->btnPlayerType->set_visible(true);
2747 d->btnPlayerType->set_enabled(stateaccess);
2748
2749- if (player.state == PlayerSettings::stateOpen) {
2750+ if (player.state == PlayerSettings::State::kOpen) {
2751 d->btnPlayerType->set_title(_("Open"));
2752 d->btnPlayerTeam->set_visible(false);
2753 d->btnPlayerTeam->set_visible(false);
2754@@ -144,7 +144,7 @@
2755 } else {
2756 std::string title;
2757
2758- if (player.state == PlayerSettings::stateComputer) {
2759+ if (player.state == PlayerSettings::State::kComputer) {
2760 if (player.ai.empty())
2761 title = _("Computer");
2762 else {
2763@@ -156,7 +156,7 @@
2764 title = _(impl->descname);
2765 }
2766 }
2767- } else { // PlayerSettings::stateHuman
2768+ } else { // PlayerSettings::State::stateHuman
2769 title = _("Human");
2770 }
2771 d->btnPlayerType->set_title(title);
2772@@ -215,11 +215,11 @@
2773 return;
2774
2775 if (on) {
2776- if (settings.players[d->plnum].state == PlayerSettings::stateClosed)
2777+ if (settings.players[d->plnum].state == PlayerSettings::State::kClosed)
2778 d->settings->next_player_state(d->plnum);
2779 } else {
2780- if (settings.players[d->plnum].state != PlayerSettings::stateClosed)
2781- d->settings->set_player_state(d->plnum, PlayerSettings::stateClosed);
2782+ if (settings.players[d->plnum].state != PlayerSettings::State::kClosed)
2783+ d->settings->set_player_state(d->plnum, PlayerSettings::State::kClosed);
2784 }
2785 }
2786