GunChleoc has proposed merging lp:~widelands-dev/widelands/multiplayer_dropdowns into lp:widelands with lp:~widelands-dev/widelands/multiplayer_dropdowns_2_init_team as a prerequisite.
Requested reviews: Widelands Developers (widelands-dev) Related bugs: Bug #1407396 in widelands: "MP Game Setup - UI errors" https://bugs.launchpad.net/widelands/+bug/1407396 For more details, see: https://code.launchpad.net/~widelands-dev/widelands/multiplayer_dropdowns/+merge/326302 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 -- Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/multiplayer_dropdowns into lp:widelands.
=== added file 'data/images/players/no_team.png' Binary files data/images/players/no_team.png 1970-01-01 00:00:00 +0000 and data/images/players/no_team.png 2017-06-26 14:45:57 +0000 differ === added file 'data/images/players/team.png' Binary files data/images/players/team.png 1970-01-01 00:00:00 +0000 and data/images/players/team.png 2017-06-26 14:45:57 +0000 differ === added file 'data/images/players/team_pc.png' Binary files data/images/players/team_pc.png 1970-01-01 00:00:00 +0000 and data/images/players/team_pc.png 2017-06-26 14:45:57 +0000 differ === modified file 'src/ai/computer_player.cc' --- src/ai/computer_player.cc 2017-01-25 18:55:59 +0000 +++ src/ai/computer_player.cc 2017-06-26 14:45:57 +0000 @@ -36,12 +36,13 @@ } struct EmptyAIImpl : Implementation { - EmptyAIImpl() { - name = "empty"; - /** TRANSLATORS: This is the name of an AI used in the game setup screens */ - descname = _("No AI"); - icon_filename = "images/ai/ai_empty.png"; - type = Implementation::Type::kEmpty; + EmptyAIImpl() + : Implementation( + "empty", + /** TRANSLATORS: This is the name of an AI used in the game setup screens */ + _("No AI"), + "images/ai/ai_empty.png", + Implementation::Type::kEmpty) { } ComputerPlayer* instantiate(Widelands::Game& g, Widelands::PlayerNumber const pid) const override { === modified file 'src/ai/computer_player.h' --- src/ai/computer_player.h 2017-04-22 08:02:21 +0000 +++ src/ai/computer_player.h 2017-06-26 14:45:57 +0000 @@ -23,9 +23,15 @@ #include <string> #include <vector> +#include <boost/algorithm/string/predicate.hpp> + #include "base/macros.h" #include "logic/widelands.h" +// We need to use a string prefix in the game setup screens to identify the AIs, so we make sure +// that the AI names don't contain the separator that's used to parse the strings there. +#define AI_NAME_SEPARATOR "|" + namespace Widelands { class Game; } // namespace Widelands @@ -61,6 +67,17 @@ std::string descname; std::string icon_filename; Type type; + explicit Implementation(std::string init_name, + std::string init_descname, + std::string init_icon_filename, + Type init_type) + : name(init_name), + descname(init_descname), + icon_filename(init_icon_filename), + type(init_type) { + assert(!boost::contains(name, AI_NAME_SEPARATOR)); + } + virtual ~Implementation() { } virtual ComputerPlayer* instantiate(Widelands::Game&, Widelands::PlayerNumber) const = 0; === modified file 'src/ai/defaultai.h' --- src/ai/defaultai.h 2017-01-25 18:55:59 +0000 +++ src/ai/defaultai.h 2017-06-26 14:45:57 +0000 @@ -96,12 +96,13 @@ /// Implementation for Strong struct NormalImpl : public ComputerPlayer::Implementation { - NormalImpl() { - name = "normal"; - /** TRANSLATORS: This is the name of an AI used in the game setup screens */ - descname = _("Normal AI"); - icon_filename = "images/ai/ai_normal.png"; - type = Implementation::Type::kDefault; + NormalImpl() + : Implementation( + "normal", + /** TRANSLATORS: This is the name of an AI used in the game setup screens */ + _("Normal AI"), + "images/ai/ai_normal.png", + Implementation::Type::kDefault) { } ComputerPlayer* instantiate(Widelands::Game& game, Widelands::PlayerNumber const p) const override { @@ -110,12 +111,13 @@ }; struct WeakImpl : public ComputerPlayer::Implementation { - WeakImpl() { - name = "weak"; - /** TRANSLATORS: This is the name of an AI used in the game setup screens */ - descname = _("Weak AI"); - icon_filename = "images/ai/ai_weak.png"; - type = Implementation::Type::kDefault; + WeakImpl() + : Implementation( + "weak", + /** TRANSLATORS: This is the name of an AI used in the game setup screens */ + _("Weak AI"), + "images/ai/ai_weak.png", + Implementation::Type::kDefault) { } ComputerPlayer* instantiate(Widelands::Game& game, Widelands::PlayerNumber const p) const override { @@ -124,12 +126,13 @@ }; struct VeryWeakImpl : public ComputerPlayer::Implementation { - VeryWeakImpl() { - name = "very_weak"; - /** TRANSLATORS: This is the name of an AI used in the game setup screens */ - descname = _("Very Weak AI"); - icon_filename = "images/ai/ai_very_weak.png"; - type = Implementation::Type::kDefault; + VeryWeakImpl() + : Implementation( + "very_weak", + /** TRANSLATORS: This is the name of an AI used in the game setup screens */ + _("Very Weak AI"), + "images/ai/ai_very_weak.png", + Implementation::Type::kDefault) { } ComputerPlayer* instantiate(Widelands::Game& game, Widelands::PlayerNumber const p) const override { === modified file 'src/base/macros.h' --- src/base/macros.h 2017-06-23 08:33:03 +0000 +++ src/base/macros.h 2017-06-26 14:45:57 +0000 @@ -104,4 +104,8 @@ // the side-effect upcast has of creating a new identifier which won't be used. #define is_a(type, source) (dynamic_cast<const type*>(source) != nullptr) +// For printf placeholders, we need to cast int8_t/uint8_t to avoid confusion with char +#define cast_unsigned(u) static_cast<unsigned int>(u) +#define cast_signed(i) static_cast<int>(i) + #endif // end of include guard: WL_BASE_MACROS_H === modified file 'src/graphic/playercolor.h' --- src/graphic/playercolor.h 2017-04-28 06:47:01 +0000 +++ src/graphic/playercolor.h 2017-06-26 14:45:57 +0000 @@ -49,6 +49,19 @@ RGBColor(144, 144, 144), // light gray }; +// Hard coded team colors +const RGBColor kTeamColors[kMaxPlayers / 2 + 1] = { + RGBColor(100, 100, 100), // No team + RGBColor(2, 2, 198), // blue + RGBColor(255, 41, 0), // red + RGBColor(255, 232, 0), // yellow + RGBColor(59, 223, 3), // green + RGBColor(57, 57, 57), // black/dark gray + RGBColor(255, 172, 0), // orange + RGBColor(215, 0, 218), // purple + RGBColor(255, 255, 255), // white +}; + /// Looks for a player color mask image, and if it finds one, /// returns the image with added playercolor. If no player color /// image file is found, gets the image from 'image_filename' === modified file 'src/logic/CMakeLists.txt' --- src/logic/CMakeLists.txt 2017-06-05 07:48:28 +0000 +++ src/logic/CMakeLists.txt 2017-06-26 14:45:57 +0000 @@ -13,6 +13,7 @@ io_filesystem logic logic_constants + notifications scripting_lua_interface scripting_lua_table ) === modified file 'src/logic/game.cc' --- src/logic/game.cc 2017-06-25 08:20:25 +0000 +++ src/logic/game.cc 2017-06-26 14:45:57 +0000 @@ -265,10 +265,10 @@ for (uint32_t i = 0; i < settings.players.size(); ++i) { const PlayerSettings& playersettings = settings.players[i]; - if (playersettings.state == PlayerSettings::stateClosed || - playersettings.state == PlayerSettings::stateOpen) + if (playersettings.state == PlayerSettings::State::kClosed || + playersettings.state == PlayerSettings::State::kOpen) continue; - else if (playersettings.state == PlayerSettings::stateShared) { + else if (playersettings.state == PlayerSettings::State::kShared) { shared.push_back(playersettings); shared_num.push_back(i + 1); continue; === modified file 'src/logic/game_settings.cc' --- src/logic/game_settings.cc 2017-06-05 07:48:28 +0000 +++ src/logic/game_settings.cc 2017-06-26 14:45:57 +0000 @@ -1,1 +1,38 @@ -// Dummy to make CMake happy +/* + * Copyright (C) 2017 by the Widelands Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "logic/game_settings.h" + +Widelands::PlayerNumber GameSettings::find_shared(PlayerSlot slot) const { + Widelands::PlayerNumber result = 1; + for (; result <= players.size(); ++result) { + if (PlayerSettings::can_be_shared(players.at(result - 1).state) && (result - 1) != slot) { + break; + } + } + return result; +} + +bool GameSettings::is_shared_usable(PlayerSlot slot, Widelands::PlayerNumber shared) const { + return shared <= players.size() && (shared - 1) != slot; +} + +bool GameSettings::uncloseable(PlayerSlot slot) const { + return (scenario && !players.at(slot).closeable) || savegame; +} === modified file 'src/logic/game_settings.h' --- src/logic/game_settings.h 2017-06-05 07:33:18 +0000 +++ src/logic/game_settings.h 2017-06-26 14:45:57 +0000 @@ -28,11 +28,22 @@ #include "logic/map_objects/tribes/tribe_basic_info.h" #include "logic/player_end_result.h" #include "logic/widelands.h" +#include "notifications/note_ids.h" +#include "notifications/notifications.h" #include "scripting/lua_interface.h" #include "scripting/lua_table.h" +// PlayerSlot 0 will give us Widelands::PlayerNumber 1 etc., so we rename it to avoid confusion. +// TODO(GunChleoc): Rename all uint8_t to PlayerSlot or Widelands::PlayerNumber +using PlayerSlot = Widelands::PlayerNumber; + struct PlayerSettings { - enum State { stateOpen, stateHuman, stateComputer, stateClosed, stateShared }; + enum class State { kOpen, kHuman, kComputer, kClosed, kShared }; + + /// Returns whether the given state allows sharing a slot at all + static bool can_be_shared(PlayerSettings::State state) { + return state != PlayerSettings::State::kClosed && state != PlayerSettings::State::kShared; + } State state; uint8_t initialization_index; @@ -71,6 +82,28 @@ // not }; +/// The gamehost/gameclient are sending those to notify about status ghanges, which are then picked +/// up by the UI. +struct NoteGameSettings { + CAN_BE_SENT_AS_NOTE(NoteId::GameSettings) + + enum class Action { + kUser, // A client has picked a different player slot / become an observer + kPlayer, // A player slot has changed its status (type, tribe etc.) + kMap // An new map/savegame was selected + }; + + Action action; + PlayerSlot position; + uint8_t usernum; + + explicit NoteGameSettings(Action init_action, + PlayerSlot init_position = std::numeric_limits<uint8_t>::max(), + uint8_t init_usernum = UserSettings::none()) + : action(init_action), position(init_position), usernum(init_usernum) { + } +}; + /** * Holds all settings about a game that can be configured before the * game actually starts. @@ -92,6 +125,14 @@ } } + /// Find a player number that the slot could share in. Does not guarantee that a viable slot was + /// actually found. + Widelands::PlayerNumber find_shared(PlayerSlot slot) const; + /// Check if the player number returned by find_shared is usable + bool is_shared_usable(PlayerSlot slot, Widelands::PlayerNumber shared) const; + /// Savegame slots and certain scenario slots can't be closed + bool uncloseable(PlayerSlot slot) const; + /// Number of player position int16_t playernum; /// Number of users entry @@ -154,7 +195,9 @@ bool savegame = false) = 0; virtual void set_player_state(uint8_t number, PlayerSettings::State) = 0; virtual void set_player_ai(uint8_t number, const std::string&, bool const random_ai = false) = 0; - virtual void next_player_state(uint8_t number) = 0; + // Multiplayer no longer toggles per button + virtual void next_player_state(uint8_t /* number */) { + } virtual void set_player_tribe(uint8_t number, const std::string&, bool const random_tribe = false) = 0; virtual void set_player_init(uint8_t number, uint8_t index) = 0; @@ -163,10 +206,11 @@ virtual void set_player_number(uint8_t number) = 0; virtual void set_player_team(uint8_t number, Widelands::TeamNumber team) = 0; virtual void set_player_closeable(uint8_t number, bool closeable) = 0; - virtual void set_player_shared(uint8_t number, uint8_t shared) = 0; + virtual void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) = 0; virtual void set_win_condition_script(const std::string& wc) = 0; virtual std::string get_win_condition_script() = 0; + // For retrieveing tips texts struct NoTribe {}; const std::string& get_players_tribe() { if (UserSettings::highest_playernum() < settings().playernum) === modified file 'src/logic/single_player_game_settings_provider.cc' --- src/logic/single_player_game_settings_provider.cc 2017-02-10 14:12:36 +0000 +++ src/logic/single_player_game_settings_provider.cc 2017-06-26 14:45:57 +0000 @@ -81,14 +81,15 @@ while (oldplayers < maxplayers) { PlayerSettings& player = s.players[oldplayers]; - player.state = (oldplayers == 0) ? PlayerSettings::stateHuman : PlayerSettings::stateComputer; + player.state = + (oldplayers == 0) ? PlayerSettings::State::kHuman : PlayerSettings::State::kComputer; player.tribe = s.tribes.at(0).name; player.random_tribe = false; player.initialization_index = 0; player.name = (boost::format(_("Player %u")) % (oldplayers + 1)).str(); player.team = 0; // Set default computerplayer ai type - if (player.state == PlayerSettings::stateComputer) { + if (player.state == PlayerSettings::State::kComputer) { const ComputerPlayer::ImplementationVector& impls = ComputerPlayer::get_implementations(); if (impls.size() > 1) { player.ai = impls.at(0)->name; @@ -107,8 +108,8 @@ if (number == s.playernum || number >= s.players.size()) return; - if (state == PlayerSettings::stateOpen) - state = PlayerSettings::stateComputer; + if (state == PlayerSettings::State::kOpen) + state = PlayerSettings::State::kComputer; s.players[number].state = state; } @@ -147,7 +148,7 @@ s.players[number].ai = (*it)->name; } - s.players[number].state = PlayerSettings::stateComputer; + s.players[number].state = PlayerSettings::State::kComputer; } void SinglePlayerGameSettingsProvider::set_player_tribe(uint8_t const number, @@ -199,7 +200,7 @@ // nothing to do } -void SinglePlayerGameSettingsProvider::set_player_shared(uint8_t, uint8_t) { +void SinglePlayerGameSettingsProvider::set_player_shared(PlayerSlot, Widelands::PlayerNumber) { // nothing to do } @@ -219,8 +220,8 @@ return; PlayerSettings const position = settings().players.at(number); PlayerSettings const player = settings().players.at(settings().playernum); - if (number < settings().players.size() && (position.state == PlayerSettings::stateOpen || - position.state == PlayerSettings::stateComputer)) { + if (number < settings().players.size() && (position.state == PlayerSettings::State::kOpen || + position.state == PlayerSettings::State::kComputer)) { set_player(number, player); set_player(settings().playernum, position); s.playernum = number; === modified file 'src/logic/single_player_game_settings_provider.h' --- src/logic/single_player_game_settings_provider.h 2017-02-10 14:12:36 +0000 +++ src/logic/single_player_game_settings_provider.h 2017-06-26 14:45:57 +0000 @@ -54,7 +54,7 @@ void set_player_init(uint8_t const number, uint8_t const index) override; void set_player_team(uint8_t number, Widelands::TeamNumber team) override; void set_player_closeable(uint8_t, bool) override; - void set_player_shared(uint8_t, uint8_t) override; + void set_player_shared(PlayerSlot, Widelands::PlayerNumber) override; void set_player_name(uint8_t const number, const std::string& name) override; void set_player(uint8_t const number, const PlayerSettings& ps) override; void set_player_number(uint8_t const number) override; === modified file 'src/network/CMakeLists.txt' --- src/network/CMakeLists.txt 2017-05-14 21:06:23 +0000 +++ src/network/CMakeLists.txt 2017-06-26 14:45:57 +0000 @@ -33,7 +33,6 @@ build_info chat game_io - graphic_playercolor helper io_fileread io_filesystem === modified file 'src/network/gameclient.cc' --- src/network/gameclient.cc 2017-06-15 15:45:01 +0000 +++ src/network/gameclient.cc 2017-06-26 14:45:57 +0000 @@ -349,25 +349,25 @@ // client is not allowed to do this } -void GameClient::set_player_shared(uint8_t number, uint8_t player) { +void GameClient::set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) { if ((number != d->settings.playernum)) return; SendPacket s; s.unsigned_8(NETCMD_SETTING_CHANGESHARED); s.unsigned_8(number); - s.unsigned_8(player); + s.unsigned_8(shared); d->net->send(s); } -void GameClient::set_player_init(uint8_t number, uint8_t) { +void GameClient::set_player_init(uint8_t number, uint8_t initialization_index) { if ((number != d->settings.playernum)) return; - // Host will decide what to change, therefore the init is not send, just the request to change SendPacket s; s.unsigned_8(NETCMD_SETTING_CHANGEINIT); s.unsigned_8(number); + s.unsigned_8(initialization_index); d->net->send(s); } @@ -397,8 +397,8 @@ return; // Same if the player is not selectable if (number < d->settings.players.size() && - (d->settings.players.at(number).state == PlayerSettings::stateClosed || - d->settings.players.at(number).state == PlayerSettings::stateComputer)) + (d->settings.players.at(number).state == PlayerSettings::State::kClosed || + d->settings.players.at(number).state == PlayerSettings::State::kComputer)) return; // Send request @@ -452,6 +452,7 @@ player.random_ai = packet.unsigned_8() == 1; player.team = packet.unsigned_8(); player.shared_in = packet.unsigned_8(); + Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kPlayer, number)); } void GameClient::receive_one_user(uint32_t const number, StreamRead& packet) { @@ -466,10 +467,13 @@ d->settings.users.at(number).name = packet.string(); d->settings.users.at(number).position = packet.signed_32(); d->settings.users.at(number).ready = packet.unsigned_8() == 1; + if (static_cast<int32_t>(number) == d->settings.usernum) { d->localplayername = d->settings.users.at(number).name; d->settings.playernum = d->settings.users.at(number).position; } + Notifications::publish( + NoteGameSettings(NoteGameSettings::Action::kUser, d->settings.playernum, number)); } void GameClient::send(const std::string& msg) { @@ -560,6 +564,7 @@ // New map was set, so we clean up the buffer of a previously requested file if (file_) delete file_; + Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kMap)); break; } === modified file 'src/network/gameclient.h' --- src/network/gameclient.h 2017-06-15 15:45:01 +0000 +++ src/network/gameclient.h 2017-06-26 14:45:57 +0000 @@ -85,13 +85,13 @@ virtual void set_player_tribe(uint8_t number, const std::string& tribe, bool const random_tribe = false) override; - void set_player_init(uint8_t number, uint8_t index) override; + void set_player_init(uint8_t number, uint8_t initialization_index) override; void set_player_name(uint8_t number, const std::string& name) override; void set_player(uint8_t number, const PlayerSettings& ps) override; void set_player_number(uint8_t number) override; void set_player_team(uint8_t number, Widelands::TeamNumber team) override; void set_player_closeable(uint8_t number, bool closeable) override; - void set_player_shared(uint8_t number, uint8_t shared) override; + void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) override; void set_win_condition_script(const std::string&) override; std::string get_win_condition_script() override; === modified file 'src/network/gamehost.cc' --- src/network/gamehost.cc 2017-06-25 21:55:39 +0000 +++ src/network/gamehost.cc 2017-06-26 14:45:57 +0000 @@ -81,13 +81,16 @@ return true; } bool can_change_player_state(uint8_t const number) override { + if (number >= settings().players.size()) { + return false; + } if (settings().savegame) - return settings().players.at(number).state != PlayerSettings::stateClosed; + return settings().players.at(number).state != PlayerSettings::State::kClosed; else if (settings().scenario) - return ((settings().players.at(number).state == PlayerSettings::stateOpen || - settings().players.at(number).state == PlayerSettings::stateHuman) && + return ((settings().players.at(number).state == PlayerSettings::State::kOpen || + settings().players.at(number).state == PlayerSettings::State::kHuman) && settings().players.at(number).closeable) || - settings().players.at(number).state == PlayerSettings::stateClosed; + settings().players.at(number).state == PlayerSettings::State::kClosed; return true; } bool can_change_player_tribe(uint8_t const number) override { @@ -105,7 +108,7 @@ return false; if (number == settings().playernum) return true; - return settings().players.at(number).state == PlayerSettings::stateComputer; + return settings().players.at(number).state == PlayerSettings::State::kComputer; } bool can_launch() override { @@ -124,83 +127,6 @@ host_->set_player_state(number, state); } - void next_player_state(uint8_t const number) override { - if (number > settings().players.size()) - return; - - PlayerSettings::State newstate = PlayerSettings::stateClosed; - switch (host_->settings().players.at(number).state) { - case PlayerSettings::stateClosed: - // In savegames : closed players can not be changed. - assert(!host_->settings().savegame); - newstate = PlayerSettings::stateOpen; - break; - case PlayerSettings::stateOpen: - case PlayerSettings::stateHuman: - if (host_->settings().scenario) { - assert(host_->settings().players.at(number).closeable); - newstate = PlayerSettings::stateClosed; - break; - } // else fall through - FALLS_THROUGH; - case PlayerSettings::stateComputer: { - const ComputerPlayer::ImplementationVector& impls = ComputerPlayer::get_implementations(); - ComputerPlayer::ImplementationVector::const_iterator it = impls.begin(); - if (host_->settings().players.at(number).ai.empty()) { - set_player_ai(number, (*it)->name); - newstate = PlayerSettings::stateComputer; - break; - } - do { - ++it; - if ((*(it - 1))->name == host_->settings().players.at(number).ai) - break; - } while (it != impls.end()); - if (settings().players.at(number).random_ai) { - set_player_ai(number, std::string()); - set_player_name(number, std::string()); - // Do not share a player in savegames or scenarios - if (host_->settings().scenario || host_->settings().savegame) - newstate = PlayerSettings::stateOpen; - else { - uint8_t shared = 0; - for (; shared < settings().players.size(); ++shared) { - if (settings().players.at(shared).state != PlayerSettings::stateClosed && - settings().players.at(shared).state != PlayerSettings::stateShared) - break; - } - if (shared < settings().players.size()) { - newstate = PlayerSettings::stateShared; - set_player_shared(number, shared + 1); - } else - newstate = PlayerSettings::stateClosed; - } - } else if (it == impls.end()) { - do { - uint8_t random = (std::rand() % impls.size()); // Choose a random AI - it = impls.begin() + random; - } while ((*it)->type == ComputerPlayer::Implementation::Type::kEmpty); - set_player_ai(number, (*it)->name, true); - newstate = PlayerSettings::stateComputer; - break; - } else { - set_player_ai(number, (*it)->name); - newstate = PlayerSettings::stateComputer; - } - break; - } - case PlayerSettings::stateShared: { - // Do not close a player in savegames or scenarios - if (host_->settings().scenario || host_->settings().savegame) - newstate = PlayerSettings::stateOpen; - else - newstate = PlayerSettings::stateClosed; - break; - } - } - - host_->set_player_state(number, newstate, true); - } void set_player_tribe(uint8_t number, const std::string& tribe, bool const random_tribe) override { @@ -208,9 +134,10 @@ return; if (number == settings().playernum || - settings().players.at(number).state == PlayerSettings::stateComputer || - settings().players.at(number).state == PlayerSettings::stateShared || - settings().players.at(number).state == PlayerSettings::stateOpen) // For savegame loading + settings().players.at(number).state == PlayerSettings::State::kComputer || + settings().players.at(number).state == PlayerSettings::State::kShared || + settings().players.at(number).state == + PlayerSettings::State::kOpen) // For savegame loading host_->set_player_tribe(number, tribe, random_tribe); } @@ -219,7 +146,7 @@ return; if (number == settings().playernum || - settings().players.at(number).state == PlayerSettings::stateComputer) + settings().players.at(number).state == PlayerSettings::State::kComputer) host_->set_player_team(number, team); } @@ -229,7 +156,7 @@ host_->set_player_closeable(number, closeable); } - void set_player_shared(uint8_t number, uint8_t shared) override { + void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) override { if (number >= host_->settings().players.size()) return; host_->set_player_shared(number, shared); @@ -1024,9 +951,9 @@ // but not all should be closed! bool one_not_closed = false; for (PlayerSettings& setting : d->settings.players) { - if (setting.state != PlayerSettings::stateClosed) + if (setting.state != PlayerSettings::State::kClosed) one_not_closed = true; - if (setting.state == PlayerSettings::stateOpen) + if (setting.state == PlayerSettings::State::kOpen) return false; } return one_not_closed; @@ -1076,7 +1003,7 @@ while (oldplayers < maxplayers) { PlayerSettings& player = d->settings.players.at(oldplayers); - player.state = PlayerSettings::stateOpen; + player.state = PlayerSettings::State::kOpen; player.name = ""; player.tribe = d->settings.tribes.at(0).name; player.random_tribe = false; @@ -1151,9 +1078,7 @@ if (player.state == state) return; - SendPacket s; - - if (player.state == PlayerSettings::stateHuman) { + if (player.state == PlayerSettings::State::kHuman) { // 0 is host and has no client if (d->settings.users.at(0).position == number) { d->settings.users.at(0).position = UserSettings::none(); @@ -1173,13 +1098,6 @@ break; } } - - // broadcast change - s.unsigned_8(NETCMD_SETTING_USER); - s.unsigned_32(i); - write_setting_user(s, i); - broadcast(s); - break; } } @@ -1187,15 +1105,53 @@ player.state = state; - if (player.state == PlayerSettings::stateComputer) + // Make sure that shared slots have a player number to share in + if (player.state == PlayerSettings::State::kShared) { + const PlayerSlot shared = d->settings.find_shared(number); + if (d->settings.is_shared_usable(number, shared)) { + set_player_shared(number, shared); + } else { + player.state = PlayerSettings::State::kClosed; + } + } + + // Update shared positions for other players + for (size_t i = 0; i < d->settings.players.size(); ++i) { + if (i == number) { + // Don't set own state + continue; + } + if (d->settings.players.at(i).state == PlayerSettings::State::kShared) { + const PlayerSlot shared = d->settings.find_shared(i); + if (d->settings.is_shared_usable(i, shared)) { + set_player_shared(i, shared); + } else { + set_player_state(i, PlayerSettings::State::kClosed, host); + } + } + } + + // Make sure that slots that are not closeable stay open + if (player.state == PlayerSettings::State::kClosed && d->settings.uncloseable(number)) { + player.state = PlayerSettings::State::kOpen; + } + + if (player.state == PlayerSettings::State::kComputer) player.name = get_computer_player_name(number); - // Broadcast change + // Broadcast change to player + SendPacket s; s.reset(); s.unsigned_8(NETCMD_SETTING_PLAYER); s.unsigned_8(number); write_setting_player(s, number); broadcast(s); + + // Let clients know whether their slot has changed + s.reset(); + s.unsigned_8(NETCMD_SETTING_ALLUSERS); + write_setting_all_users(s); + broadcast(s); } void GameHost::set_player_tribe(uint8_t const number, @@ -1317,7 +1273,7 @@ // uses it. } -void GameHost::set_player_shared(uint8_t number, uint8_t shared) { +void GameHost::set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared) { if (number >= d->settings.players.size()) return; @@ -1327,8 +1283,8 @@ return; PlayerSettings& sharedplr = d->settings.players.at(shared - 1); - assert(sharedplr.state != PlayerSettings::stateClosed && - sharedplr.state != PlayerSettings::stateShared); + assert(PlayerSettings::can_be_shared(sharedplr.state)); + assert(d->settings.is_shared_usable(number, shared)); player.shared_in = shared; player.tribe = sharedplr.tribe; @@ -1372,8 +1328,8 @@ void GameHost::switch_to_player(uint32_t user, uint8_t number) { if (number < d->settings.players.size() && - (d->settings.players.at(number).state != PlayerSettings::stateOpen && - d->settings.players.at(number).state != PlayerSettings::stateHuman)) + (d->settings.players.at(number).state != PlayerSettings::State::kOpen && + d->settings.players.at(number).state != PlayerSettings::State::kHuman)) return; uint32_t old = d->settings.users.at(user).position; @@ -1388,14 +1344,14 @@ temp2 = temp2.erase(op.name.find(temp), temp.size()); set_player_name(old, temp2); if (temp2.empty()) - set_player_state(old, PlayerSettings::stateOpen); + set_player_state(old, PlayerSettings::State::kOpen); } if (number < d->settings.players.size()) { // Add clients name to new player slot PlayerSettings& op = d->settings.players.at(number); - if (op.state == PlayerSettings::stateOpen) { - set_player_state(number, PlayerSettings::stateHuman); + if (op.state == PlayerSettings::State::kOpen) { + set_player_state(number, PlayerSettings::State::kHuman); set_player_name(number, " " + name + " "); } else set_player_name(number, op.name + " " + name + " "); @@ -1482,6 +1438,7 @@ packet.string(d->settings.mapfilename); packet.unsigned_8(d->settings.savegame ? 1 : 0); packet.unsigned_8(d->settings.scenario ? 1 : 0); + Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kMap)); } void GameHost::write_setting_player(SendPacket& packet, uint8_t const number) { @@ -1495,6 +1452,7 @@ packet.unsigned_8(player.random_ai ? 1 : 0); packet.unsigned_8(player.team); packet.unsigned_8(player.shared_in); + Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kPlayer, number)); } void GameHost::write_setting_all_players(SendPacket& packet) { @@ -1507,6 +1465,8 @@ packet.string(d->settings.users.at(number).name); packet.signed_32(d->settings.users.at(number).position); packet.unsigned_8(d->settings.users.at(number).ready ? 1 : 0); + Notifications::publish(NoteGameSettings( + NoteGameSettings::Action::kUser, d->settings.users.at(number).position, number)); } void GameHost::write_setting_all_users(SendPacket& packet) { @@ -1678,7 +1638,7 @@ // Check if there is an unoccupied player left and if, assign. for (uint8_t i = 0; i < d->settings.players.size(); ++i) - if (d->settings.players.at(i).state == PlayerSettings::stateOpen) { + if (d->settings.players.at(i).state == PlayerSettings::State::kOpen) { switch_to_player(client.usernum, i); break; } @@ -2100,10 +2060,13 @@ case NETCMD_SETTING_CHANGEINIT: if (!d->game) { + // TODO(GunChleoc): For some nebulous reason, we don't receive the num that the client is + // sending when a player changes slot. So, keeping the access to the client off for now. + // Would be nice to have though. uint8_t num = r.unsigned_8(); if (num != client.playernum) throw DisconnectException("NO_ACCESS_TO_PLAYER"); - d->npsb.toggle_init(num); + set_player_init(num, r.unsigned_8()); } break; @@ -2273,7 +2236,7 @@ } } - set_player_state(number, PlayerSettings::stateOpen); + set_player_state(number, PlayerSettings::State::kOpen); if (d->game) init_computer_player(number + 1); } === modified file 'src/network/gamehost.h' --- src/network/gamehost.h 2017-06-24 11:18:12 +0000 +++ src/network/gamehost.h 2017-06-26 14:45:57 +0000 @@ -76,7 +76,7 @@ void set_player_number(uint8_t number); void set_player_team(uint8_t number, Widelands::TeamNumber team); void set_player_closeable(uint8_t number, bool closeable); - void set_player_shared(uint8_t number, uint8_t shared); + void set_player_shared(PlayerSlot number, Widelands::PlayerNumber shared); void switch_to_player(uint32_t user, uint8_t number); void set_win_condition_script(const std::string& wc); === modified file 'src/network/network_player_settings_backend.cc' --- src/network/network_player_settings_backend.cc 2017-02-14 10:18:15 +0000 +++ src/network/network_player_settings_backend.cc 2017-06-26 14:45:57 +0000 @@ -19,134 +19,69 @@ #include "network/network_player_settings_backend.h" -#include "base/i18n.h" -#include "base/log.h" -#include "base/wexception.h" -#include "logic/game_settings.h" -#include "logic/map_objects/tribes/tribe_descr.h" -#include "logic/player.h" - -/// Toggle through the types -void NetworkPlayerSettingsBackend::toggle_type(uint8_t id) { - if (id >= s->settings().players.size()) - return; - - s->next_player_state(id); -} - -void NetworkPlayerSettingsBackend::set_tribe(uint8_t id, const std::string& tribename) { +#include "ai/computer_player.h" + +void NetworkPlayerSettingsBackend::set_player_state(PlayerSlot id, PlayerSettings::State state) { + if (id >= s->settings().players.size()) { + return; + } + s->set_player_state(id, state); +} + +void NetworkPlayerSettingsBackend::set_player_ai(PlayerSlot id, + const std::string& name, + bool random_ai) { + if (id >= s->settings().players.size()) { + return; + } + if (random_ai) { + const ComputerPlayer::ImplementationVector& impls = ComputerPlayer::get_implementations(); + ComputerPlayer::ImplementationVector::const_iterator it = impls.begin(); + if (impls.size() > 1) { + do { + size_t random = (std::rand() % impls.size()); // Choose a random AI + it = impls.begin() + random; + } while ((*it)->type == ComputerPlayer::Implementation::Type::kEmpty); + } + s->set_player_ai(id, (*it)->name, random_ai); + } else { + s->set_player_ai(id, name, random_ai); + } +} + +void NetworkPlayerSettingsBackend::set_player_tribe(PlayerSlot id, const std::string& tribename) { const GameSettings& settings = s->settings(); - - if (id >= settings.players.size() || tribename.empty()) + if (id >= settings.players.size() || tribename.empty()) { return; - - if (settings.players.at(id).state != PlayerSettings::stateShared) { + } + if (settings.players.at(id).state != PlayerSettings::State::kShared) { s->set_player_tribe(id, tribename, tribename == "random"); } } /// Set the shared in player for the given id -void NetworkPlayerSettingsBackend::set_shared_in(uint8_t id, uint8_t shared_in) { - const GameSettings& settings = s->settings(); - if (id > settings.players.size() || shared_in > settings.players.size()) - return; - if (settings.players.at(id).state == PlayerSettings::stateShared) { - s->set_player_shared(id, shared_in); - } -} - -/// Toggle through shared in players -void NetworkPlayerSettingsBackend::toggle_shared_in(uint8_t id) { - const GameSettings& settings = s->settings(); - - if (id >= settings.players.size() || - settings.players.at(id).state != PlayerSettings::stateShared) - return; - - uint8_t sharedplr = settings.players.at(id).shared_in; - for (; sharedplr < settings.players.size(); ++sharedplr) { - if (settings.players.at(sharedplr).state != PlayerSettings::stateClosed && - settings.players.at(sharedplr).state != PlayerSettings::stateShared) - break; - } - if (sharedplr < settings.players.size()) { - // We have already found the next player - set_shared_in(id, sharedplr + 1); - return; - } - sharedplr = 0; - for (; sharedplr < settings.players.at(id).shared_in; ++sharedplr) { - if (settings.players.at(sharedplr).state != PlayerSettings::stateClosed && - settings.players.at(sharedplr).state != PlayerSettings::stateShared) - break; - } - if (sharedplr < settings.players.at(id).shared_in) { - // We have found the next player - set_shared_in(id, sharedplr + 1); - return; - } else { - // No fitting player found - return toggle_type(id); - } -} - -/// Toggle through the initializations -void NetworkPlayerSettingsBackend::toggle_init(uint8_t id) { - const GameSettings& settings = s->settings(); - - if (id >= settings.players.size()) - return; - - const PlayerSettings& player = settings.players[id]; - for (const TribeBasicInfo& temp_tribeinfo : settings.tribes) { - if (temp_tribeinfo.name == player.tribe) { - return s->set_player_init( - id, (player.initialization_index + 1) % temp_tribeinfo.initializations.size()); - } - } - NEVER_HERE(); -} - -/// Toggle through the teams -void NetworkPlayerSettingsBackend::toggle_team(uint8_t id) { - const GameSettings& settings = s->settings(); - - if (id >= settings.players.size()) - return; - - Widelands::TeamNumber currentteam = settings.players.at(id).team; - Widelands::TeamNumber maxteam = settings.players.size() / 2; - Widelands::TeamNumber newteam; - - if (currentteam >= maxteam) - newteam = 0; - else - newteam = currentteam + 1; - - s->set_player_team(id, newteam); -} - -/// Check if all settings for the player are still valid -void NetworkPlayerSettingsBackend::refresh(uint8_t id) { - const GameSettings& settings = s->settings(); - - if (id >= settings.players.size()) - return; - - const PlayerSettings& player = settings.players[id]; - - if (player.state == PlayerSettings::stateShared) { - // ensure that the shared_in player is able to use this starting position - if (player.shared_in > settings.players.size()) - toggle_shared_in(id); - if (settings.players.at(player.shared_in - 1).state == PlayerSettings::stateClosed || - settings.players.at(player.shared_in - 1).state == PlayerSettings::stateShared) - toggle_shared_in(id); - - if (shared_in_tribe[id] != settings.players.at(player.shared_in - 1).tribe) { - s->set_player_tribe(id, settings.players.at(player.shared_in - 1).tribe, - settings.players.at(player.shared_in - 1).random_tribe); - shared_in_tribe[id] = settings.players.at(id).tribe; - } - } +void NetworkPlayerSettingsBackend::set_player_shared(PlayerSlot id, + Widelands::PlayerNumber shared) { + const GameSettings& settings = s->settings(); + if (id >= settings.players.size() || shared > settings.players.size()) + return; + if (settings.players.at(id).state == PlayerSettings::State::kShared) { + s->set_player_shared(id, shared); + } +} + +/// Sets the initialization for the player slot (Headquarters, Fortified Village etc.) +void NetworkPlayerSettingsBackend::set_player_init(PlayerSlot id, uint8_t initialization_index) { + if (id >= s->settings().players.size()) { + return; + } + s->set_player_init(id, initialization_index); +} + +/// Sets the team for the player slot +void NetworkPlayerSettingsBackend::set_player_team(PlayerSlot id, Widelands::TeamNumber team) { + if (id >= s->settings().players.size()) { + return; + } + s->set_player_team(id, team); } === modified file 'src/network/network_player_settings_backend.h' --- src/network/network_player_settings_backend.h 2017-06-24 08:47:46 +0000 +++ src/network/network_player_settings_backend.h 2017-06-26 14:45:57 +0000 @@ -20,33 +20,22 @@ #ifndef WL_NETWORK_NETWORK_PLAYER_SETTINGS_BACKEND_H #define WL_NETWORK_NETWORK_PLAYER_SETTINGS_BACKEND_H -#include "graphic/playercolor.h" #include "logic/game_settings.h" +#include "logic/widelands.h" struct NetworkPlayerSettingsBackend { explicit NetworkPlayerSettingsBackend(GameSettingsProvider* const settings) : s(settings) { - for (uint8_t i = 0; i < kMaxPlayers; ++i) - shared_in_tribe[i] = std::string(); - } - - void toggle_type(uint8_t id); - void set_shared_in(uint8_t id, uint8_t shared_in); - void set_tribe(uint8_t id, const std::string& tribename); - void set_block_tribe_selection(bool blocked) { - tribe_selection_blocked = blocked; - } - - void toggle_init(uint8_t id); - void toggle_team(uint8_t id); - void refresh(uint8_t id); + } + + void set_player_state(PlayerSlot id, PlayerSettings::State state); + void set_player_ai(PlayerSlot id, const std::string& name, bool random_ai); + void set_player_shared(PlayerSlot id, Widelands::PlayerNumber shared); + void set_player_tribe(PlayerSlot id, const std::string& tribename); + void set_player_init(PlayerSlot id, uint8_t initialization_index); + void set_player_team(PlayerSlot id, Widelands::TeamNumber team); GameSettingsProvider* const s; - std::string shared_in_tribe[kMaxPlayers]; - bool tribe_selection_blocked; - -private: - void toggle_shared_in(uint8_t id); }; #endif // end of include guard: WL_NETWORK_NETWORK_PLAYER_SETTINGS_BACKEND_H === modified file 'src/network/network_protocol.h' --- src/network/network_protocol.h 2017-05-06 11:00:19 +0000 +++ src/network/network_protocol.h 2017-06-26 14:45:57 +0000 @@ -395,9 +395,9 @@ * \li unsigned_8: new shared player * * \note The client must not assume that the host will accept this - * request. Change of team number only becomes effective when/if the host + * request. Change of the initialization only becomes effective when/if the host * replies with a \ref NETCMD_SETTING_PLAYER or - * \ref NETCMD_SETTING_ALLPLAYERS indicating the changed team. + * \ref NETCMD_SETTING_ALLPLAYERS indicating the changed initialization. */ NETCMD_SETTING_CHANGESHARED = 27, @@ -406,6 +406,7 @@ * client wants to change a player's initialisation. * * \li unsigned_8: number of the player + * \li unsigned_8: index of the initialization * * \note The client must not assume that the host will accept this * request. Change of team number only becomes effective when/if the host === modified file 'src/notifications/note_ids.h' --- src/notifications/note_ids.h 2017-03-02 12:21:57 +0000 +++ src/notifications/note_ids.h 2017-06-26 14:45:57 +0000 @@ -40,7 +40,9 @@ Economy, GraphicResolutionChanged, NoteExpeditionCanceled, - Sound + Sound, + Dropdown, + GameSettings }; #endif // end of include guard: WL_NOTIFICATIONS_NOTE_IDS_H === modified file 'src/ui_basic/dropdown.cc' --- src/ui_basic/dropdown.cc 2017-06-02 08:14:40 +0000 +++ src/ui_basic/dropdown.cc 2017-06-26 14:45:57 +0000 @@ -43,6 +43,8 @@ namespace UI { +int BaseDropdown::next_id_ = 0; + BaseDropdown::BaseDropdown(UI::Panel* parent, int32_t x, int32_t y, @@ -56,11 +58,14 @@ : UI::Panel(parent, x, y, - type == DropdownType::kTextual ? w : button_dimension, + type == DropdownType::kPictorial ? button_dimension : w, // Height only to fit the button, so we can use this in Box layout. base_height(button_dimension)), + id_(next_id_++), max_list_height_(h - 2 * get_h()), list_width_(w), + list_offset_x_(0), + list_offset_y_(0), button_dimension_(button_dimension), mouse_tolerance_(50), button_box_(this, 0, 0, UI::Box::Horizontal, w, h), @@ -79,13 +84,23 @@ "dropdown_label", 0, 0, - type == DropdownType::kTextual ? w - button_dimension : button_dimension, + type == DropdownType::kTextual ? + w - button_dimension : + type == DropdownType::kTextualNarrow ? w : button_dimension, get_h(), background, label), label_(label), type_(type), is_enabled_(true) { + + // Close whenever another dropdown is opened + subscriber_ = Notifications::subscribe<NoteDropdown>([this](const NoteDropdown& note) { + if (id_ != note.id) { + close(); + } + }); + assert(max_list_height_ > 0); // Hook into highest parent that we can get so that we can drop down outside the panel. // Positioning breaks down with TabPanels, so we exclude them. @@ -125,26 +140,42 @@ void BaseDropdown::layout() { const int base_h = base_height(button_dimension_); - const int w = type_ == DropdownType::kTextual ? get_w() : button_dimension_; + const int w = type_ == DropdownType::kPictorial ? button_dimension_ : get_w(); button_box_.set_size(w, base_h); display_button_.set_desired_size( type_ == DropdownType::kTextual ? w - button_dimension_ : w, base_h); int new_list_height = std::min(static_cast<int>(list_->size()) * list_->get_lineheight(), max_list_height_); - list_->set_size(type_ == DropdownType::kTextual ? w : list_width_, new_list_height); + list_->set_size(type_ != DropdownType::kPictorial ? w : list_width_, new_list_height); set_desired_size(w, base_h); // Update list position. The list is hooked into the highest parent that we can get so that we // can drop down outside the panel. Positioning breaks down with TabPanels, so we exclude them. UI::Panel* parent = get_parent(); - int new_list_y = get_y() + get_h() + parent->get_y(); + int new_list_y = get_y() + parent->get_y(); int new_list_x = get_x() + parent->get_x(); while (parent->get_parent() && !is_a(UI::TabPanel, parent->get_parent())) { parent = parent->get_parent(); new_list_y += parent->get_y(); new_list_x += parent->get_x(); } - list_->set_pos(Vector2i(new_list_x, new_list_y)); + + // Drop up instead of down if it doesn't fit + if (new_list_y + list_->get_h() > g_gr->get_yres()) { + list_offset_y_ = -list_->get_h(); + } else { + list_offset_y_ = display_button_.get_h(); + } + + // Right align instead of left align if it doesn't fit + if (new_list_x + list_->get_w() > g_gr->get_xres()) { + list_offset_x_ = display_button_.get_w() - list_->get_w(); + if (push_button_ != nullptr) { + list_offset_x_ += push_button_->get_w(); + } + } + + list_->set_pos(Vector2i(new_list_x + list_offset_x_, new_list_y + list_offset_y_)); } void BaseDropdown::add(const std::string& name, @@ -178,16 +209,29 @@ void BaseDropdown::set_label(const std::string& text) { label_ = text; - if (type_ == DropdownType::kTextual) { + if (type_ != DropdownType::kPictorial) { display_button_.set_title(label_); } } +void BaseDropdown::set_image(const Image* image) { + display_button_.set_pic(image); +} + void BaseDropdown::set_tooltip(const std::string& text) { tooltip_ = text; display_button_.set_tooltip(tooltip_); } +void BaseDropdown::set_errored(const std::string& error_message) { + set_tooltip((boost::format(_("%1%: %2%")) % _("Error") % error_message).str()); + if (type_ != DropdownType::kPictorial) { + set_label(_("Error")); + } else { + set_image(g_gr->images().get("images/ui_basic/different.png")); + } +} + void BaseDropdown::set_enabled(bool on) { is_enabled_ = on; set_can_focus(on); @@ -213,6 +257,7 @@ } void BaseDropdown::clear() { + close(); list_->clear(); current_selection_ = list_->selection_index(); list_->set_size(list_->get_w(), 0); @@ -239,7 +284,7 @@ /** TRANSLATORS: Selection in Dropdown menus. */ pgettext("dropdown", "Not Selected"); - if (type_ == DropdownType::kTextual) { + if (type_ != DropdownType::kPictorial) { if (label_.empty()) { display_button_.set_title(name); } else { @@ -268,19 +313,29 @@ return; } list_->set_visible(!list_->is_visible()); + if (type_ != DropdownType::kTextual) { + display_button_.set_perm_pressed(list_->is_visible()); + } if (list_->is_visible()) { list_->move_to_top(); focus(); + Notifications::publish(NoteDropdown(id_)); } // Make sure that the list covers and deactivates the elements below it set_layout_toplevel(list_->is_visible()); } +void BaseDropdown::close() { + if (is_expanded()) { + toggle_list(); + } +} + bool BaseDropdown::is_mouse_away() const { - return (get_mouse_position().x + mouse_tolerance_) < 0 || - get_mouse_position().x > (list_->get_w() + mouse_tolerance_) || - (get_mouse_position().y + mouse_tolerance_ / 2) < 0 || - get_mouse_position().y > (get_h() + list_->get_h() + mouse_tolerance_); + return (get_mouse_position().x + mouse_tolerance_) < list_offset_x_ || + get_mouse_position().x > (list_offset_x_ + list_->get_w() + mouse_tolerance_) || + (get_mouse_position().y + mouse_tolerance_ / 2) < list_offset_y_ || + get_mouse_position().y > (list_offset_y_ + get_h() + list_->get_h() + mouse_tolerance_); } bool BaseDropdown::handle_key(bool down, SDL_Keysym code) { === modified file 'src/ui_basic/dropdown.h' --- src/ui_basic/dropdown.h 2017-04-21 09:56:42 +0000 +++ src/ui_basic/dropdown.h 2017-06-26 14:45:57 +0000 @@ -27,14 +27,26 @@ #include "graphic/graphic.h" #include "graphic/image.h" +#include "notifications/note_ids.h" +#include "notifications/notifications.h" #include "ui_basic/box.h" #include "ui_basic/button.h" #include "ui_basic/listselect.h" #include "ui_basic/panel.h" namespace UI { - -enum class DropdownType { kTextual, kPictorial }; +// We use this to make sure that only 1 dropdown is open at the same time. +struct NoteDropdown { + CAN_BE_SENT_AS_NOTE(NoteId::Dropdown) + + int id; + + explicit NoteDropdown(int init_id) : id(init_id) { + } +}; + +/// The narrow textual dropdown omits the extra push button +enum class DropdownType { kTextual, kTextualNarrow, kPictorial }; /// Implementation for a dropdown menu that lets the user select a value. class BaseDropdown : public Panel { @@ -63,6 +75,7 @@ ~BaseDropdown(); public: + /// An entry was selected boost::signals2::signal<void()> selected; /// \return true if an element has been selected from the list @@ -72,9 +85,15 @@ /// and displayed on the display button. void set_label(const std::string& text); + /// Sets the image for the display button (for pictorial dropdowns). + void set_image(const Image* image); + /// Sets the tooltip for the display button. void set_tooltip(const std::string& text); + /// Displays an error message on the button instead of the current selection. + void set_errored(const std::string& error_message); + /// Enables/disables the dropdown selection. void set_enabled(bool on); @@ -142,12 +161,22 @@ void set_value(); /// Toggles the dropdown list on and off. void toggle_list(); + /// Toggle the list closed if the dropdown is currently expanded. + void close(); /// Returns true if the mouse pointer left the vicinity of the dropdown. bool is_mouse_away() const; + /// Give each dropdown a unique ID + static int next_id_; + const int id_; + std::unique_ptr<Notifications::Subscriber<NoteDropdown>> subscriber_; + + // Dimensions int max_list_height_; int list_width_; + int list_offset_x_; + int list_offset_y_; int button_dimension_; const int mouse_tolerance_; // Allow mouse outside the panel a bit before autocollapse UI::Box button_box_; === modified file 'src/ui_basic/listselect.cc' --- src/ui_basic/listselect.cc 2017-06-15 16:34:58 +0000 +++ src/ui_basic/listselect.cc 2017-06-26 14:45:57 +0000 @@ -359,9 +359,9 @@ if (selection_mode_ == ListselectLayout::kDropdown) { RGBAColor black(0, 0, 0, 255); // top edge - dst.brighten_rect(Recti(0, 0, get_w(), 2), BUTTON_EDGE_BRIGHT_FACTOR / 4); + dst.brighten_rect(Recti(0, 0, get_w(), 2), BUTTON_EDGE_BRIGHT_FACTOR); // left edge - dst.brighten_rect(Recti(0, 0, 2, get_h()), BUTTON_EDGE_BRIGHT_FACTOR); + dst.brighten_rect(Recti(0, 2, 2, get_h()), BUTTON_EDGE_BRIGHT_FACTOR); // bottom edge dst.fill_rect(Recti(2, get_h() - 2, get_eff_w() - 2, 1), black); dst.fill_rect(Recti(1, get_h() - 1, get_eff_w() - 1, 1), black); === modified file 'src/ui_fsmenu/launch_game.cc' --- src/ui_fsmenu/launch_game.cc 2017-05-18 06:52:04 +0000 +++ src/ui_fsmenu/launch_game.cc 2017-06-26 14:45:57 +0000 @@ -167,7 +167,7 @@ t->get_string("description")); } } catch (LuaTableKeyError& e) { - log("LaunchSPG: Error loading win condition: %s %s\n", win_condition_script.c_str(), + log("Launch Game: Error loading win condition: %s %s\n", win_condition_script.c_str(), e.what()); } } @@ -177,9 +177,8 @@ "could not be loaded.")) % settings_->settings().mapfilename) .str(); - win_condition_dropdown_.set_label(_("Error")); - win_condition_dropdown_.set_tooltip(error_message); - log("LaunchSPG: Exception: %s %s\n", error_message.c_str(), e.what()); + win_condition_dropdown_.set_errored(error_message); + log("Launch Game: Exception: %s %s\n", error_message.c_str(), e.what()); } } @@ -202,8 +201,8 @@ } } } catch (LuaTableKeyError& e) { - log( - "LaunchSPG: Error loading win condition: %s %s\n", win_condition_script.c_str(), e.what()); + log("Launch Game: Error loading win condition: %s %s\n", win_condition_script.c_str(), + e.what()); } if (!is_usable) { t.reset(nullptr); === modified file 'src/ui_fsmenu/launch_mpg.cc' --- src/ui_fsmenu/launch_mpg.cc 2017-05-18 06:52:04 +0000 +++ src/ui_fsmenu/launch_mpg.cc 2017-06-26 14:45:57 +0000 @@ -181,7 +181,7 @@ map_info_.set_text(_("The host has not yet selected a map or saved game.")); mpsg_ = new MultiPlayerSetupGroup( - this, get_w() / 50, get_h() / 8, get_w() * 57 / 80, get_h(), settings, butw_, buth_); + this, get_w() / 50, change_map_or_save_.get_y(), get_w() * 57 / 80, get_h(), settings, buth_); // If we are the host, open the map or save selection menu at startup if (settings_->settings().usernum == 0 && settings_->settings().mapname.empty()) { @@ -355,6 +355,8 @@ * buttons and text. */ void FullscreenMenuLaunchMPG::refresh() { + // TODO(GunChleoc): Investigate what we can handle with NoteGameSettings. Maybe we can get rid of + // refresh() and thus think(). const GameSettings& settings = settings_->settings(); if (settings.mapfilename != filename_proof_) { @@ -425,8 +427,6 @@ } win_condition_dropdown_.set_enabled(false); } - // Update the multi player setup group - mpsg_->refresh(); } /** @@ -443,16 +443,21 @@ map.set_filename(settings.mapfilename); ml->preload_map(true); Widelands::PlayerNumber const nrplayers = map.get_nrplayers(); + if (settings.players.size() != nrplayers) { + // Due to asynchronous notifications, the client can crash when and update is missing and the + // number of players is wrong. + return; + } for (uint8_t i = 0; i < nrplayers; ++i) { settings_->set_player_tribe(i, map.get_scenario_player_tribe(i + 1)); settings_->set_player_closeable(i, map.get_scenario_player_closeable(i + 1)); std::string ai(map.get_scenario_player_ai(i + 1)); if (!ai.empty()) { - settings_->set_player_state(i, PlayerSettings::stateComputer); + settings_->set_player_state(i, PlayerSettings::State::kComputer); settings_->set_player_ai(i, ai); - } else if (settings.players.at(i).state != PlayerSettings::stateHuman && - settings.players.at(i).state != PlayerSettings::stateOpen) { - settings_->set_player_state(i, PlayerSettings::stateOpen); + } else if (settings.players.at(i).state != PlayerSettings::State::kHuman && + settings.players.at(i).state != PlayerSettings::State::kOpen) { + settings_->set_player_state(i, PlayerSettings::State::kOpen); } } } @@ -470,22 +475,25 @@ std::string player_save_tribe[kMaxPlayers]; std::string player_save_ai[kMaxPlayers]; - uint8_t i = 1; - for (; i <= nr_players_; ++i) { + for (uint8_t i = 1; i <= nr_players_; ++i) { + Section* s = prof.get_section((boost::format("player_%u") % cast_unsigned(i)).str()); + if (s == nullptr) { + // Due to asynchronous notifications, the client can crash on savegame change when number + // of players goes down. So, we abort if the section does not exists to prevent crashes. + return; + } + player_save_name[i - 1] = s->get_string("name"); + player_save_tribe[i - 1] = s->get_string("tribe"); + player_save_ai[i - 1] = s->get_string("ai"); + infotext += "\n* "; - Section& s = - prof.get_safe_section((boost::format("player_%u") % static_cast<unsigned int>(i)).str()); - player_save_name[i - 1] = s.get_string("name"); - player_save_tribe[i - 1] = s.get_string("tribe"); - player_save_ai[i - 1] = s.get_string("ai"); - infotext += (boost::format(_("Player %u")) % static_cast<unsigned int>(i)).str(); if (player_save_tribe[i - 1].empty()) { std::string closed_string = (boost::format("<%s>") % _("closed")).str(); infotext += ":\n "; infotext += closed_string; // Close the player - settings_->set_player_state(i - 1, PlayerSettings::stateClosed); + settings_->set_player_state(i - 1, PlayerSettings::State::kClosed); continue; // if tribe is empty, the player does not exist } @@ -495,10 +503,10 @@ if (player_save_ai[i - 1].empty()) { // Assure that player is open - if (settings_->settings().players.at(i - 1).state != PlayerSettings::stateHuman) - settings_->set_player_state(i - 1, PlayerSettings::stateOpen); + if (settings_->settings().players.at(i - 1).state != PlayerSettings::State::kHuman) + settings_->set_player_state(i - 1, PlayerSettings::State::kOpen); } else { - settings_->set_player_state(i - 1, PlayerSettings::stateComputer); + settings_->set_player_state(i - 1, PlayerSettings::State::kComputer); settings_->set_player_ai(i - 1, player_save_ai[i - 1]); } @@ -563,7 +571,9 @@ "\n"; infotext += std::string("• ") + - (boost::format(ngettext("%u Player", "%u Players", nr_players_)) % nr_players_).str() + "\n"; + (boost::format(ngettext("%u Player", "%u Players", nr_players_)) % cast_unsigned(nr_players_)) + .str() + + "\n"; if (settings_->settings().scenario) infotext += std::string("• ") + (boost::format(_("Scenario mode selected"))).str() + "\n"; infotext += "\n"; === modified file 'src/ui_fsmenu/launch_mpg.h' --- src/ui_fsmenu/launch_mpg.h 2017-02-10 14:12:36 +0000 +++ src/ui_fsmenu/launch_mpg.h 2017-06-26 14:45:57 +0000 @@ -23,6 +23,7 @@ #include <memory> #include <string> +#include "logic/game_settings.h" #include "ui_basic/button.h" #include "ui_basic/dropdown.h" #include "ui_basic/multilinetextarea.h" === modified file 'src/ui_fsmenu/launch_spg.cc' --- src/ui_fsmenu/launch_spg.cc 2017-05-04 05:08:14 +0000 +++ src/ui_fsmenu/launch_spg.cc 2017-06-26 14:45:57 +0000 @@ -210,8 +210,8 @@ for (uint8_t i = 0; i < nr_players_; ++i) { pos_[i]->set_visible(true); const PlayerSettings& player = settings.players[i]; - pos_[i]->set_enabled(!is_scenario_ && (player.state == PlayerSettings::stateOpen || - player.state == PlayerSettings::stateComputer)); + pos_[i]->set_enabled(!is_scenario_ && (player.state == PlayerSettings::State::kOpen || + player.state == PlayerSettings::State::kComputer)); } for (uint32_t i = nr_players_; i < kMaxPlayers; ++i) pos_[i]->set_visible(false); @@ -291,14 +291,14 @@ // Check if a still valid place is open. for (uint8_t i = 0; i < newplayernumber; ++i) { PlayerSettings position = settings.players.at(i); - if (position.state == PlayerSettings::stateOpen) { + if (position.state == PlayerSettings::State::kOpen) { switch_to_position(i); return; } } // Kick player 1 and take the position - settings_->set_player_state(0, PlayerSettings::stateClosed); - settings_->set_player_state(0, PlayerSettings::stateOpen); + settings_->set_player_state(0, PlayerSettings::State::kClosed); + settings_->set_player_state(0, PlayerSettings::State::kOpen); switch_to_position(0); } === modified file 'src/wui/multiplayersetupgroup.cc' --- src/wui/multiplayersetupgroup.cc 2017-05-03 07:24:06 +0000 +++ src/wui/multiplayersetupgroup.cc 2017-06-26 14:45:57 +0000 @@ -19,8 +19,10 @@ #include "wui/multiplayersetupgroup.h" +#include <memory> #include <string> +#include <boost/algorithm/string.hpp> #include <boost/format.hpp> #include <boost/lexical_cast.hpp> @@ -36,419 +38,551 @@ #include "logic/map_objects/tribes/tribe_descr.h" #include "logic/map_objects/tribes/tribes.h" #include "logic/player.h" +#include "logic/widelands.h" #include "ui_basic/button.h" -#include "ui_basic/checkbox.h" #include "ui_basic/dropdown.h" -#include "ui_basic/icon.h" +#include "ui_basic/mouse_constants.h" #include "ui_basic/scrollbar.h" #include "ui_basic/textarea.h" +#define AI_NAME_PREFIX "ai" AI_NAME_SEPARATOR + +constexpr int kPadding = 4; + +/// Holds the info and dropdown menu for a connected client struct MultiPlayerClientGroup : public UI::Box { MultiPlayerClientGroup(UI::Panel* const parent, - uint8_t id, - int32_t const /* x */, - int32_t const /* y */, int32_t const w, int32_t const h, + PlayerSlot id, GameSettingsProvider* const settings) - : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h), - type_icon(nullptr), - type(nullptr), + : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h, kPadding), + slot_dropdown_(this, 0, 0, h, 200, h, _("Role"), UI::DropdownType::kPictorial), + // Name needs to be initialized after the dropdown, otherwise the layout function will + // crash. + name(new UI::Textarea(this, 0, 0, w - h - UI::Scrollbar::kSize * 11 / 5, h)), s(settings), id_(id), - save_(-2) { + slot_selection_locked_(false) { set_size(w, h); - name = new UI::Textarea(this, 0, 0, w - h - UI::Scrollbar::kSize * 11 / 5, h); - add(name); - // Either Button if changeable OR text if not - if (id == settings->settings().usernum) { // Our Client - type = new UI::Button( - this, "client_type", 0, 0, h, h, g_gr->images().get("images/ui_basic/but1.png"), ""); - type->sigclicked.connect( - boost::bind(&MultiPlayerClientGroup::toggle_type, boost::ref(*this))); - add(type); - } else { // just a shown client - type_icon = new UI::Icon( - this, 0, 0, h, h, g_gr->images().get("images/wui/fieldaction/menu_tab_watch.png")); - add(type_icon); - } - } - - /// Switch human players and spectator - void toggle_type() { - UserSettings us = s->settings().users.at(id_); - int16_t p = us.position; - if (p == UserSettings::none()) - p = -1; - - for (++p; p < static_cast<int16_t>(s->settings().players.size()); ++p) { - if (s->settings().players.at(p).state == PlayerSettings::stateHuman || - s->settings().players.at(p).state == PlayerSettings::stateOpen) { - s->set_player_number(p); - return; - } - } - s->set_player_number(UserSettings::none()); - } - - /// Care about visibility and current values - void refresh() { - UserSettings us = s->settings().users.at(id_); - if (us.position == UserSettings::not_connected()) { - name->set_text((boost::format("<%s>") % _("free")).str()); - if (type) - type->set_visible(false); - else - type_icon->set_visible(false); - } else { - name->set_text(us.name); - if (save_ != us.position) { - const Image* position_image; - std::string temp_tooltip; - if (us.position < UserSettings::highest_playernum()) { - position_image = - playercolor_image(us.position, "images/players/genstats_player.png"); - temp_tooltip = - (boost::format(_("Player %u")) % static_cast<unsigned int>(us.position + 1)) - .str(); - } else { - position_image = g_gr->images().get("images/wui/fieldaction/menu_tab_watch.png"); - temp_tooltip = _("Spectator"); - } - - // Either Button if changeable OR text if not - if (id_ == s->settings().usernum) { - type->set_pic(position_image); - type->set_tooltip(temp_tooltip); - type->set_visible(true); - } else { - type_icon->set_icon(position_image); - type_icon->set_tooltip(temp_tooltip); - type_icon->set_visible(true); - } - save_ = us.position; - } - } - } - - UI::Textarea* name; - UI::Icon* type_icon; - UI::Button* type; + + add(&slot_dropdown_); + add(name, UI::Box::Resizing::kAlign, UI::Align::kCenter); + + slot_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat); + slot_dropdown_.selected.connect( + boost::bind(&MultiPlayerClientGroup::set_slot, boost::ref(*this))); + + update(); + layout(); + + subscriber_ = + Notifications::subscribe<NoteGameSettings>([this](const NoteGameSettings& note) { + switch (note.action) { + case NoteGameSettings::Action::kMap: + /// In case the client gets kicked off its slot due to number of player slots in the + /// map + update(); + break; + case NoteGameSettings::Action::kUser: + /// Player slot might have been closed, bumping the client to observer status + if (id_ == note.usernum || note.usernum == UserSettings::none()) { + update(); + } + break; + case NoteGameSettings::Action::kPlayer: + break; + } + }); + } + + /// Update dropdown sizes + void layout() override { + UI::Box::layout(); + slot_dropdown_.set_height(g_gr->get_yres() * 3 / 4); + } + + /// This will update the client's player slot with the value currently selected in the slot + /// dropdown. + void set_slot() { + const GameSettings& settings = s->settings(); + if (id_ != settings.usernum) { + return; + } + slot_selection_locked_ = true; + if (slot_dropdown_.has_selection()) { + const uint8_t new_slot = slot_dropdown_.get_selected(); + if (new_slot != settings.users.at(id_).position) { + s->set_player_number(slot_dropdown_.get_selected()); + } + } + slot_selection_locked_ = false; + } + + /// Rebuild the slot dropdown from the server settings. This will keep the host and client UIs in + /// sync. + void rebuild_slot_dropdown(const GameSettings& settings) { + if (slot_selection_locked_) { + return; + } + const UserSettings& user_setting = settings.users.at(id_); + + slot_dropdown_.clear(); + for (PlayerSlot slot = 0; slot < settings.players.size(); ++slot) { + if (settings.players.at(slot).state == PlayerSettings::State::kHuman || + settings.players.at(slot).state == PlayerSettings::State::kOpen) { + slot_dropdown_.add((boost::format(_("Player %u")) % cast_unsigned(slot + 1)).str(), + slot, playercolor_image(slot, "images/players/genstats_player.png"), + slot == user_setting.position); + } + } + slot_dropdown_.add(_("Spectator"), UserSettings::none(), + g_gr->images().get("images/wui/fieldaction/menu_tab_watch.png"), + user_setting.position == UserSettings::none()); + slot_dropdown_.set_visible(true); + slot_dropdown_.set_enabled(id_ == settings.usernum); + } + + /// Take care of visibility and current values + void update() { + const GameSettings& settings = s->settings(); + const UserSettings& user_setting = settings.users.at(id_); + + if (user_setting.position == UserSettings::not_connected()) { + set_visible(false); + return; + } + + name->set_text(user_setting.name); + rebuild_slot_dropdown(settings); + } + + UI::Dropdown<uintptr_t> slot_dropdown_; /// Select the player slot. + UI::Textarea* name; /// Client nick name GameSettingsProvider* const s; - uint8_t const id_; - int16_t save_; // saved position to check rewrite need. + uint8_t const id_; /// User number + bool slot_selection_locked_; // Ensure that dropdowns will close on selection. + std::unique_ptr<Notifications::Subscriber<NoteGameSettings>> subscriber_; }; +/// Holds the dropdown menus for a player slot struct MultiPlayerPlayerGroup : public UI::Box { MultiPlayerPlayerGroup(UI::Panel* const parent, - uint8_t id, + PlayerSlot id, int32_t const /* x */, int32_t const /* y */, int32_t const w, int32_t const h, GameSettingsProvider* const settings, NetworkPlayerSettingsBackend* const npsb) - : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h), - player(nullptr), - type(nullptr), - init(nullptr), + : UI::Box(parent, 0, 0, UI::Box::Horizontal, w, h, kPadding / 2), s(settings), n(npsb), id_(id), + player(this, + "player", + 0, + 0, + h, + h, + g_gr->images().get("images/ui_basic/but1.png"), + playercolor_image(id, "images/players/player_position_menu.png"), + (boost::format(_("Player %u")) % cast_unsigned(id_ + 1)).str(), + UI::Button::Style::kFlat), + type_dropdown_(this, 0, 0, 50, 200, h, _("Type"), UI::DropdownType::kPictorial), tribes_dropdown_(this, 0, 0, 50, 200, h, _("Tribe"), UI::DropdownType::kPictorial), - last_state_(PlayerSettings::stateClosed), - last_player_amount_(0) { + init_dropdown_( + this, 0, 0, w - 4 * h - 3 * kPadding, 200, h, "", UI::DropdownType::kTextualNarrow), + team_dropdown_(this, 0, 0, h, 200, h, _("Team"), UI::DropdownType::kPictorial), + last_state_(PlayerSettings::State::kClosed), + type_selection_locked_(false), + tribe_selection_locked_(false), + init_selection_locked_(false), + team_selection_locked_(false) { set_size(w, h); - tribes_dropdown_.set_visible(false); - tribes_dropdown_.set_enabled(false); + + player.set_disable_style(UI::ButtonDisableStyle::kFlat); + player.set_enabled(false); + + type_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat); + tribes_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat); + init_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat); + team_dropdown_.set_disable_style(UI::ButtonDisableStyle::kFlat); + + type_dropdown_.selected.connect( + boost::bind(&MultiPlayerPlayerGroup::set_type, boost::ref(*this))); tribes_dropdown_.selected.connect( boost::bind(&MultiPlayerPlayerGroup::set_tribe_or_shared_in, boost::ref(*this))); + init_dropdown_.selected.connect( + boost::bind(&MultiPlayerPlayerGroup::set_init, boost::ref(*this))); + team_dropdown_.selected.connect( + boost::bind(&MultiPlayerPlayerGroup::set_team, boost::ref(*this))); - const Image* player_image = playercolor_image(id, "images/players/player_position_menu.png"); - assert(player_image); - player = new UI::Icon(this, 0, 0, h, h, player_image); - add(player); - type = new UI::Button( - this, "player_type", 0, 0, h, h, g_gr->images().get("images/ui_basic/but1.png"), ""); - type->sigclicked.connect( - boost::bind(&MultiPlayerPlayerGroup::toggle_type, boost::ref(*this))); - add(type); + add_space(0); + add(&player); + add(&type_dropdown_); add(&tribes_dropdown_); - init = new UI::Button(this, "player_init", 0, 0, w - 4 * h, h, - g_gr->images().get("images/ui_basic/but1.png"), ""); - init->sigclicked.connect( - boost::bind(&MultiPlayerPlayerGroup::toggle_init, boost::ref(*this))); - add(init); - team = new UI::Button( - this, "player_team", 0, 0, h, h, g_gr->images().get("images/ui_basic/but1.png"), ""); - team->sigclicked.connect( - boost::bind(&MultiPlayerPlayerGroup::toggle_team, boost::ref(*this))); - add(team); - } - - /// Toggle through the types - void toggle_type() { - n->toggle_type(id_); + add(&init_dropdown_); + add(&team_dropdown_); + add_space(0); + + subscriber_ = + Notifications::subscribe<NoteGameSettings>([this](const NoteGameSettings& note) { + switch (note.action) { + case NoteGameSettings::Action::kMap: + // We don't care about map updates, since we receive enough notifications for the + // slots. + break; + default: + if (s->settings().players.empty()) { + // No map/savegame yet + return; + } + if (id_ == note.position || + s->settings().players[id_].state == PlayerSettings::State::kShared) { + update(); + } + } + }); + + // Init dropdowns + update(); + layout(); + } + + /// Update dropdown sizes + void layout() override { + const int max_height = g_gr->get_yres() * 3 / 4; + type_dropdown_.set_height(max_height); + tribes_dropdown_.set_height(max_height); + init_dropdown_.set_height(max_height); + team_dropdown_.set_height(max_height); + UI::Box::layout(); + } + + /// This will update the game settings for the type with the value + /// currently selected in the type dropdown. + void set_type() { + if (!s->can_change_player_state(id_)) { + return; + } + type_selection_locked_ = true; + if (type_dropdown_.has_selection()) { + const std::string& selected = type_dropdown_.get_selected(); + PlayerSettings::State state = PlayerSettings::State::kComputer; + if (selected == "closed") { + state = PlayerSettings::State::kClosed; + } else if (selected == "open") { + state = PlayerSettings::State::kOpen; + } else if (selected == "shared_in") { + state = PlayerSettings::State::kShared; + } else { + if (selected == AI_NAME_PREFIX "random") { + n->set_player_ai(id_, "", true); + } else { + if (boost::starts_with(selected, AI_NAME_PREFIX)) { + std::vector<std::string> parts; + boost::split(parts, selected, boost::is_any_of(AI_NAME_SEPARATOR)); + assert(parts.size() == 2); + n->set_player_ai(id_, parts[1], false); + } else { + throw wexception("Unknown player state: %s\n", selected.c_str()); + } + } + } + n->set_player_state(id_, state); + } + type_selection_locked_ = false; + } + + /// Rebuild the type dropdown from the server settings. This will keep the host and client UIs in + /// sync. + void rebuild_type_dropdown(const GameSettings& settings) { + if (type_selection_locked_) { + return; + } + type_dropdown_.clear(); + // AIs + for (const auto* impl : ComputerPlayer::get_implementations()) { + type_dropdown_.add(_(impl->descname), + (boost::format(AI_NAME_PREFIX "%s") % impl->name).str(), + g_gr->images().get(impl->icon_filename), false, _(impl->descname)); + } + /** TRANSLATORS: This is the name of an AI used in the game setup screens */ + type_dropdown_.add(_("Random AI"), AI_NAME_PREFIX "random", + g_gr->images().get("images/ai/ai_random.png"), false, _("Random AI")); + + // Slot state. Only add shared_in if there are viable slots + if (settings.is_shared_usable(id_, settings.find_shared(id_))) { + type_dropdown_.add(_("Shared in"), "shared_in", + g_gr->images().get("images/ui_fsmenu/shared_in.png"), false, + _("Shared in")); + } + + // Do not close a player in savegames or scenarios + if (!settings.uncloseable(id_)) { + type_dropdown_.add(_("Closed"), "closed", g_gr->images().get("images/ui_basic/stop.png"), + false, _("Closed")); + } + + type_dropdown_.add( + _("Open"), "open", g_gr->images().get("images/ui_basic/continue.png"), false, _("Open")); + + type_dropdown_.set_enabled(s->can_change_player_state(id_)); + + // Now select the entry according to server settings + const PlayerSettings& player_setting = settings.players[id_]; + if (player_setting.state == PlayerSettings::State::kHuman) { + type_dropdown_.set_image(g_gr->images().get("images/wui/stats/genstats_nrworkers.png")); + type_dropdown_.set_tooltip((boost::format(_("%1%: %2%")) % _("Type") % _("Human")).str()); + } else if (player_setting.state == PlayerSettings::State::kClosed) { + type_dropdown_.select("closed"); + } else if (player_setting.state == PlayerSettings::State::kOpen) { + type_dropdown_.select("open"); + } else if (player_setting.state == PlayerSettings::State::kShared) { + type_dropdown_.select("shared_in"); + } else { + if (player_setting.state == PlayerSettings::State::kComputer) { + if (player_setting.ai.empty()) { + type_dropdown_.set_errored(_("No AI")); + } else { + if (player_setting.random_ai) { + type_dropdown_.select(AI_NAME_PREFIX "random"); + } else { + const ComputerPlayer::Implementation* impl = + ComputerPlayer::get_implementation(player_setting.ai); + type_dropdown_.select((boost::format(AI_NAME_PREFIX "%s") % impl->name).str()); + } + } + } + } + } + + /// Whether the client who is running the UI is allowed to change the tribe for this player slot. + bool has_tribe_access() { + return s->settings().players[id_].state == PlayerSettings::State::kShared ? + s->can_change_player_init(id_) : + s->can_change_player_tribe(id_); } /// This will update the game settings for the tribe or shared_in with the value /// currently selected in the tribes dropdown. void set_tribe_or_shared_in() { - n->set_block_tribe_selection(true); - tribes_dropdown_.set_disable_style(s->settings().players[id_].state == - PlayerSettings::stateShared ? + if (!has_tribe_access()) { + return; + } + const PlayerSettings& player_settings = s->settings().players[id_]; + tribe_selection_locked_ = true; + tribes_dropdown_.set_disable_style(player_settings.state == PlayerSettings::State::kShared ? UI::ButtonDisableStyle::kPermpressed : - UI::ButtonDisableStyle::kMonochrome); + UI::ButtonDisableStyle::kFlat); if (tribes_dropdown_.has_selection()) { - if (s->settings().players[id_].state == PlayerSettings::stateShared) { - n->set_shared_in( + if (player_settings.state == PlayerSettings::State::kShared) { + n->set_player_shared( id_, boost::lexical_cast<unsigned int>(tribes_dropdown_.get_selected())); } else { - n->set_tribe(id_, tribes_dropdown_.get_selected()); - } - } - n->set_block_tribe_selection(false); - } - - /// Toggle through the initializations - void toggle_init() { - n->toggle_init(id_); - } - - /// Toggle through the teams - void toggle_team() { - n->toggle_team(id_); - } - - /// Helper function to cast shared_in for use in the dropdown. - const std::string shared_in_as_string(uint8_t shared_in) { - return boost::lexical_cast<std::string>(static_cast<unsigned int>(shared_in)); - } - - /// Update the tribes dropdown from the server settings if the server setting changed. - /// This will keep the host and client UIs in sync. - void update_tribes_dropdown(const PlayerSettings& player_setting) { - if (player_setting.state == PlayerSettings::stateClosed || - player_setting.state == PlayerSettings::stateOpen) { + n->set_player_tribe(id_, tribes_dropdown_.get_selected()); + } + } + tribe_selection_locked_ = false; + } + + /// Rebuild the tribes dropdown from the server settings. This will keep the host and client UIs + /// in sync. + void rebuild_tribes_dropdown(const GameSettings& settings) { + if (tribe_selection_locked_) { + return; + } + const PlayerSettings& player_setting = settings.players[id_]; + tribes_dropdown_.clear(); + if (player_setting.state == PlayerSettings::State::kShared) { + for (size_t i = 0; i < settings.players.size(); ++i) { + if (i != id_) { + // Do not add players that are also shared_in or closed. + const PlayerSettings& other_setting = settings.players[i]; + if (!PlayerSettings::can_be_shared(other_setting.state)) { + continue; + } + + const Image* player_image = + playercolor_image(i, "images/players/player_position_menu.png"); + assert(player_image); + const std::string player_name = + /** TRANSLATORS: This is an option in multiplayer setup for sharing + another player's starting position. */ + (boost::format(_("Shared in Player %u")) % cast_unsigned(i + 1)).str(); + tribes_dropdown_.add(player_name, + boost::lexical_cast<std::string>(cast_unsigned(i + 1)), + player_image, (i + 1) == player_setting.shared_in, player_name); + } + } + tribes_dropdown_.set_enabled(tribes_dropdown_.size() > 1); + } else { + { + i18n::Textdomain td("tribes"); + for (const TribeBasicInfo& tribeinfo : Widelands::get_all_tribeinfos()) { + tribes_dropdown_.add(_(tribeinfo.descname), tribeinfo.name, + g_gr->images().get(tribeinfo.icon), false, tribeinfo.tooltip); + } + } + tribes_dropdown_.add(pgettext("tribe", "Random"), "random", + g_gr->images().get("images/ui_fsmenu/random.png"), false, + _("The tribe will be selected at random")); + if (player_setting.random_tribe) { + tribes_dropdown_.select("random"); + } else { + tribes_dropdown_.select(player_setting.tribe); + } + } + const bool has_access = has_tribe_access(); + if (tribes_dropdown_.is_enabled() != has_access) { + tribes_dropdown_.set_enabled(has_access && tribes_dropdown_.size() > 1); + } + if (player_setting.state == PlayerSettings::State::kClosed || + player_setting.state == PlayerSettings::State::kOpen) { return; } if (!tribes_dropdown_.is_visible()) { tribes_dropdown_.set_visible(true); } - if (!tribes_dropdown_.is_expanded() && !n->tribe_selection_blocked && - tribes_dropdown_.has_selection()) { - const std::string selected_tribe = tribes_dropdown_.get_selected(); - if (player_setting.state == PlayerSettings::stateShared) { - const std::string shared_in = shared_in_as_string(player_setting.shared_in); - if (shared_in != selected_tribe) { - tribes_dropdown_.select(shared_in); - } - } else { - if (player_setting.random_tribe) { - if (selected_tribe != "random") { - tribes_dropdown_.select("random"); - } - } else if (selected_tribe != player_setting.tribe) { - tribes_dropdown_.select(player_setting.tribe); - } - } - } - } - - /// If the map was changed or the selection mode changed between shared_in and tribe, rebuild the - /// dropdown. - void rebuild_tribes_dropdown(const GameSettings& settings) { - const PlayerSettings& player_setting = settings.players[id_]; - - if (player_setting.state == PlayerSettings::stateClosed || - player_setting.state == PlayerSettings::stateOpen) { - return; - } - - if (tribes_dropdown_.empty() || last_player_amount_ != settings.players.size() || - ((player_setting.state == PlayerSettings::stateShared || - last_state_ == PlayerSettings::stateShared) && - player_setting.state != last_state_)) { - tribes_dropdown_.clear(); - - // We need to see the playercolor if setting shared_in is disabled - tribes_dropdown_.set_disable_style(player_setting.state == PlayerSettings::stateShared ? - UI::ButtonDisableStyle::kPermpressed : - UI::ButtonDisableStyle::kMonochrome); - - if (player_setting.state == PlayerSettings::stateShared) { - for (size_t i = 0; i < settings.players.size(); ++i) { - if (i != id_) { - // TODO(GunChleoc): Do not add players that are also shared_in. - const Image* player_image = - playercolor_image(i, "images/players/player_position_menu.png"); - assert(player_image); - const std::string player_name = - /** TRANSLATORS: This is an option in multiplayer setup for sharing - another player's starting position. */ - (boost::format(_("Shared in Player %u")) % static_cast<unsigned int>(i + 1)) - .str(); - tribes_dropdown_.add( - player_name, shared_in_as_string(i + 1), player_image, false, player_name); - } - } - int shared_in = 0; - while (shared_in == id_) { - ++shared_in; - } - tribes_dropdown_.select(shared_in_as_string(shared_in + 1)); - tribes_dropdown_.set_enabled(tribes_dropdown_.size() > 1); - } else { - { - i18n::Textdomain td("tribes"); - for (const TribeBasicInfo& tribeinfo : Widelands::get_all_tribeinfos()) { - tribes_dropdown_.add(_(tribeinfo.descname), tribeinfo.name, - g_gr->images().get(tribeinfo.icon), false, - tribeinfo.tooltip); - } - } - tribes_dropdown_.add(pgettext("tribe", "Random"), "random", - g_gr->images().get("images/ui_fsmenu/random.png"), false, - _("The tribe will be selected at random")); - if (player_setting.random_tribe) { - tribes_dropdown_.select("random"); - } else { - tribes_dropdown_.select(player_setting.tribe); - } - } - } - last_player_amount_ = settings.players.size(); + } + + /// This will update the game settings for the initialization with the value + /// currently selected in the initialization dropdown. + void set_init() { + if (!s->can_change_player_init(id_)) { + return; + } + init_selection_locked_ = true; + if (init_dropdown_.has_selection()) { + n->set_player_init(id_, init_dropdown_.get_selected()); + } + init_selection_locked_ = false; + } + + /// Rebuild the init dropdown from the server settings. This will keep the host and client UIs in + /// sync. + void rebuild_init_dropdown(const GameSettings& settings) { + if (init_selection_locked_) { + return; + } + + init_dropdown_.clear(); + const PlayerSettings& player_setting = settings.players[id_]; + if (settings.scenario) { + init_dropdown_.set_label(_("Scenario")); + } else if (settings.savegame) { + /** Translators: This is a game type */ + init_dropdown_.set_label(_("Saved Game")); + } else { + init_dropdown_.set_label(""); + i18n::Textdomain td("tribes"); // for translated initialisation + const TribeBasicInfo tribeinfo = Widelands::get_tribeinfo(player_setting.tribe); + for (size_t i = 0; i < tribeinfo.initializations.size(); ++i) { + const TribeBasicInfo::Initialization& addme = tribeinfo.initializations[i]; + init_dropdown_.add(_(addme.descname), i, nullptr, + i == player_setting.initialization_index, _(addme.tooltip)); + } + } + + init_dropdown_.set_visible(true); + init_dropdown_.set_enabled(s->can_change_player_init(id_)); + } + + /// This will update the team settings with the value currently selected in the teams dropdown. + void set_team() { + team_selection_locked_ = true; + if (team_dropdown_.has_selection()) { + n->set_player_team(id_, team_dropdown_.get_selected()); + } + team_selection_locked_ = false; + } + + /// Rebuild the team dropdown from the server settings. This will keep the host and client UIs in + /// sync. + void rebuild_team_dropdown(const GameSettings& settings) { + if (team_selection_locked_) { + return; + } + const PlayerSettings& player_setting = settings.players[id_]; + if (player_setting.state == PlayerSettings::State::kShared) { + team_dropdown_.set_visible(false); + team_dropdown_.set_enabled(false); + return; + } + + team_dropdown_.clear(); + team_dropdown_.add(_("No Team"), 0, g_gr->images().get("images/players/no_team.png")); +#ifndef NDEBUG + const size_t no_of_team_colors = sizeof(kTeamColors) / sizeof(kTeamColors[0]); +#endif + for (Widelands::TeamNumber t = 1; t <= settings.players.size() / 2; ++t) { + assert(t < no_of_team_colors); + team_dropdown_.add((boost::format(_("Team %d")) % cast_unsigned(t)).str(), t, + playercolor_image(kTeamColors[t], "images/players/team.png")); + } + team_dropdown_.select(player_setting.team); + team_dropdown_.set_visible(true); + team_dropdown_.set_enabled(s->can_change_player_team(id_)); } /// Refresh all user interfaces - void refresh() { + void update() { const GameSettings& settings = s->settings(); - if (id_ >= settings.players.size()) { set_visible(false); return; } - n->refresh(id_); - + const PlayerSettings& player_setting = settings.players[id_]; + rebuild_type_dropdown(settings); set_visible(true); - const PlayerSettings& player_setting = settings.players[id_]; - bool typeaccess = s->can_change_player_state(id_); - bool tribeaccess = s->can_change_player_tribe(id_); - bool const initaccess = s->can_change_player_init(id_); - bool teamaccess = s->can_change_player_team(id_); - type->set_enabled(typeaccess); - - rebuild_tribes_dropdown(settings); - - if (player_setting.state == PlayerSettings::stateClosed) { - type->set_tooltip(_("Closed")); - type->set_pic(g_gr->images().get("images/ui_basic/stop.png")); - team->set_visible(false); - team->set_enabled(false); - tribes_dropdown_.set_visible(false); - tribes_dropdown_.set_enabled(false); - init->set_visible(false); - init->set_enabled(false); - return; - } else if (player_setting.state == PlayerSettings::stateOpen) { - type->set_tooltip(_("Open")); - type->set_pic(g_gr->images().get("images/ui_basic/continue.png")); - team->set_visible(false); - team->set_enabled(false); - tribes_dropdown_.set_visible(false); - tribes_dropdown_.set_enabled(false); - init->set_visible(false); - init->set_enabled(false); - return; - } else if (player_setting.state == PlayerSettings::stateShared) { - type->set_tooltip(_("Shared in")); - type->set_pic(g_gr->images().get("images/ui_fsmenu/shared_in.png")); - - update_tribes_dropdown(player_setting); - - if (tribes_dropdown_.is_enabled() != initaccess) { - tribes_dropdown_.set_enabled(initaccess && !n->tribe_selection_blocked && - tribes_dropdown_.size() > 1); - } - - team->set_visible(false); - team->set_enabled(false); - + if (player_setting.state == PlayerSettings::State::kClosed || + player_setting.state == PlayerSettings::State::kOpen) { + team_dropdown_.set_visible(false); + team_dropdown_.set_enabled(false); + tribes_dropdown_.set_visible(false); + tribes_dropdown_.set_enabled(false); + init_dropdown_.set_visible(false); + init_dropdown_.set_enabled(false); } else { - std::string title; - std::string pic = "images/"; - if (player_setting.state == PlayerSettings::stateComputer) { - if (player_setting.ai.empty()) { - title = _("Computer"); - pic += "novalue.png"; - } else { - if (player_setting.random_ai) { - /** TRANSLATORS: This is the name of an AI used in the game setup screens */ - title = _("Random AI"); - pic += "ai/ai_random.png"; - } else { - const ComputerPlayer::Implementation* impl = - ComputerPlayer::get_implementation(player_setting.ai); - title = _(impl->descname); - pic = impl->icon_filename; - } - } - } else { // PlayerSettings::stateHuman - title = _("Human"); - pic += "wui/stats/genstats_nrworkers.png"; - } - type->set_tooltip(title.c_str()); - type->set_pic(g_gr->images().get(pic)); - - update_tribes_dropdown(player_setting); - - if (tribes_dropdown_.is_enabled() != tribeaccess) { - tribes_dropdown_.set_enabled(tribeaccess && !n->tribe_selection_blocked); - } - - if (player_setting.team) { - team->set_title(std::to_string(static_cast<unsigned int>(player_setting.team))); - } else { - team->set_title("--"); - } - team->set_visible(true); - team->set_enabled(teamaccess); - } - init->set_enabled(initaccess); - init->set_visible(true); - - if (settings.scenario) - init->set_title(_("Scenario")); - else if (settings.savegame) - /** Translators: This is a game type */ - init->set_title(_("Saved Game")); - else { - i18n::Textdomain td("tribes"); // for translated initialisation - for (const TribeBasicInfo& tribeinfo : settings.tribes) { - if (tribeinfo.name == player_setting.tribe) { - init->set_title( - _(tribeinfo.initializations.at(player_setting.initialization_index).descname)); - init->set_tooltip( - _(tribeinfo.initializations.at(player_setting.initialization_index).tooltip)); - break; - } - } - } - last_state_ = player_setting.state; + rebuild_tribes_dropdown(settings); + rebuild_init_dropdown(settings); + rebuild_team_dropdown(settings); + } + + // Trigger update for the other players for shared_in mode when slots open and close + if (last_state_ != player_setting.state) { + last_state_ = player_setting.state; + for (PlayerSlot slot = 0; slot < s->settings().players.size(); ++slot) { + if (slot != id_) { + n->set_player_state(slot, settings.players[slot].state); + } + } + } } - UI::Icon* player; - UI::Button* type; - UI::Button* init; - UI::Button* team; GameSettingsProvider* const s; NetworkPlayerSettingsBackend* const n; - uint8_t const id_; + PlayerSlot const id_; + + UI::Button player; + UI::Dropdown<std::string> + type_dropdown_; /// Select who owns the slot (human, AI, open, closed, shared-in). UI::Dropdown<std::string> tribes_dropdown_; /// Select the tribe or shared_in player. - PlayerSettings::State last_state_; /// The dropdown needs updating if this changes - size_t last_player_amount_; /// The dropdown needs rebuilding if this changes + UI::Dropdown<uintptr_t> + init_dropdown_; /// Select the initialization (Headquarters, Fortified Village etc.) + UI::Dropdown<uintptr_t> team_dropdown_; /// Select the team number + PlayerSettings::State + last_state_; /// The dropdowns for the other slots need updating if this changes + /// Lock rebuilding dropdowns so that they can close on selection + bool type_selection_locked_; + bool tribe_selection_locked_; + bool init_selection_locked_; + bool team_selection_locked_; + + std::unique_ptr<Notifications::Subscriber<NoteGameSettings>> subscriber_; }; MultiPlayerSetupGroup::MultiPlayerSetupGroup(UI::Panel* const parent, @@ -457,69 +591,41 @@ int32_t const w, int32_t const h, GameSettingsProvider* const settings, - uint32_t /* butw */, uint32_t buth) - : UI::Panel(parent, x, y, w, h), + : UI::Box(parent, x, y, UI::Box::Horizontal, w, h, 8 * kPadding), s(settings), npsb(new NetworkPlayerSettingsBackend(s)), - clientbox(this, 0, buth, UI::Box::Vertical, w / 3, h - buth), - playerbox(this, w * 6 / 15, buth, UI::Box::Vertical, w * 9 / 15, h - buth), + clientbox(this, 0, 0, UI::Box::Vertical), + playerbox(this, 0, 0, UI::Box::Vertical, w * 9 / 15, h, kPadding), buth_(buth) { - int small_font = UI_FONT_SIZE_SMALL * 3 / 4; - - // Clientbox and labels - labels.push_back(new UI::Textarea( - this, UI::Scrollbar::kSize * 6 / 5, buth / 3, w / 3 - buth - UI::Scrollbar::kSize * 2, buth)); - labels.back()->set_text(_("Client name")); - labels.back()->set_fontsize(small_font); - - labels.push_back(new UI::Textarea( - this, w / 3 - buth - UI::Scrollbar::kSize * 6 / 5, buth / 3, buth * 2, buth)); - labels.back()->set_text(_("Role")); - labels.back()->set_fontsize(small_font); - - clientbox.set_size(w / 3, h - buth); + clientbox.set_size(w / 3, h); clientbox.set_scrolling(true); - // Playerbox and labels - labels.push_back(new UI::Textarea(this, w * 6 / 15, buth / 3, buth, buth)); - labels.back()->set_text(_("Start")); - labels.back()->set_fontsize(small_font); - - labels.push_back(new UI::Textarea(this, w * 6 / 15 + buth, buth / 3 - 10, buth, buth)); - labels.back()->set_text(_("Type")); - labels.back()->set_fontsize(small_font); - - labels.push_back(new UI::Textarea(this, w * 6 / 15 + buth * 2, buth / 3, buth, buth)); - labels.back()->set_text(_("Tribe")); - labels.back()->set_fontsize(small_font); - - labels.push_back(new UI::Textarea( - this, w * 6 / 15 + buth * 3, buth / 3, w * 9 / 15 - 4 * buth, buth, UI::Align::kCenter)); - labels.back()->set_text(_("Initialization")); - labels.back()->set_fontsize(small_font); - - labels.push_back(new UI::Textarea(this, w - buth, buth / 3, buth, buth, UI::Align::kRight)); - labels.back()->set_text(_("Team")); - labels.back()->set_fontsize(small_font); - - playerbox.set_size(w * 9 / 15, h - buth); + add(&clientbox, UI::Box::Resizing::kExpandBoth); + add(&playerbox); + + // Playerbox + playerbox.set_size(w * 9 / 15, h); + playerbox.add_space(0); multi_player_player_groups.resize(kMaxPlayers); - for (uint8_t i = 0; i < multi_player_player_groups.size(); ++i) { + for (PlayerSlot i = 0; i < multi_player_player_groups.size(); ++i) { multi_player_player_groups.at(i) = - new MultiPlayerPlayerGroup(&playerbox, i, 0, 0, playerbox.get_w(), buth, s, npsb.get()); + new MultiPlayerPlayerGroup(&playerbox, i, 0, 0, playerbox.get_w(), buth_, s, npsb.get()); playerbox.add(multi_player_player_groups.at(i)); } - refresh(); + playerbox.add_space(0); + + subscriber_ = + Notifications::subscribe<NoteGameSettings>([this](const NoteGameSettings&) { update(); }); + set_size(w, h); + update(); } MultiPlayerSetupGroup::~MultiPlayerSetupGroup() { } -/** - * Update display and enabled buttons based on current settings. - */ -void MultiPlayerSetupGroup::refresh() { +/// Update which slots are available based on current settings. +void MultiPlayerSetupGroup::update() { const GameSettings& settings = s->settings(); // Update / initialize client groups @@ -529,15 +635,30 @@ for (uint32_t i = 0; i < settings.users.size(); ++i) { if (!multi_player_client_groups.at(i)) { multi_player_client_groups.at(i) = - new MultiPlayerClientGroup(&clientbox, i, 0, 0, clientbox.get_w(), buth_, s); - clientbox.add( - &*multi_player_client_groups.at(i), UI::Box::Resizing::kAlign, UI::Align::kCenter); - } - multi_player_client_groups.at(i)->refresh(); - } - - // Update player groups - for (uint32_t i = 0; i < kMaxPlayers; ++i) { - multi_player_player_groups.at(i)->refresh(); + new MultiPlayerClientGroup(&clientbox, clientbox.get_w(), buth_, i, s); + clientbox.add(multi_player_client_groups.at(i), UI::Box::Resizing::kFullSize); + multi_player_client_groups.at(i)->layout(); + } + multi_player_client_groups.at(i)->set_visible(true); + } + + // Keep track of which player slots are visible + for (PlayerSlot i = 0; i < multi_player_player_groups.size(); ++i) { + const bool should_be_visible = i < settings.players.size(); + const bool is_visible = multi_player_player_groups.at(i)->is_visible(); + if (should_be_visible != is_visible) { + multi_player_player_groups.at(i)->set_visible(should_be_visible); + } + } +} + +void MultiPlayerSetupGroup::draw(RenderTarget& dst) { + for (MultiPlayerPlayerGroup* player_group : multi_player_player_groups) { + if (player_group->is_visible()) { + dst.brighten_rect( + Recti(playerbox.get_x(), playerbox.get_y() + player_group->get_y() - kPadding / 2, + playerbox.get_w() + kPadding, player_group->get_h() + kPadding), + -MOUSE_OVER_BRIGHT_FACTOR); + } } } === modified file 'src/wui/multiplayersetupgroup.h' --- src/wui/multiplayersetupgroup.h 2017-01-25 18:55:59 +0000 +++ src/wui/multiplayersetupgroup.h 2017-06-26 14:45:57 +0000 @@ -33,7 +33,6 @@ #include "ui_basic/textarea.h" struct GameSettingsProvider; -struct MultiPlayerSetupGroupOptions; struct MultiPlayerClientGroup; struct MultiPlayerPlayerGroup; @@ -44,26 +43,27 @@ * clients, computers and closed players. * */ -struct MultiPlayerSetupGroup : public UI::Panel { +struct MultiPlayerSetupGroup : public UI::Box { MultiPlayerSetupGroup(UI::Panel* parent, int32_t x, int32_t y, int32_t w, int32_t h, GameSettingsProvider* settings, - uint32_t butw, uint32_t buth); ~MultiPlayerSetupGroup(); - void refresh(); - private: + void update(); + void draw(RenderTarget& dst) override; + GameSettingsProvider* const s; std::unique_ptr<NetworkPlayerSettingsBackend> npsb; std::vector<MultiPlayerClientGroup*> multi_player_client_groups; // not owned std::vector<MultiPlayerPlayerGroup*> multi_player_player_groups; // not owned + std::unique_ptr<Notifications::Subscriber<NoteGameSettings>> subscriber_; + UI::Box clientbox, playerbox; - std::vector<UI::Textarea*> labels; uint32_t buth_; === modified file 'src/wui/playerdescrgroup.cc' --- src/wui/playerdescrgroup.cc 2017-01-30 14:40:12 +0000 +++ src/wui/playerdescrgroup.cc 2017-06-26 14:45:57 +0000 @@ -116,7 +116,7 @@ d->btnEnablePlayer->set_enabled(stateaccess); - if (player.state == PlayerSettings::stateClosed) { + if (player.state == PlayerSettings::State::kClosed) { d->btnEnablePlayer->set_state(false); d->btnPlayerTeam->set_visible(false); d->btnPlayerTeam->set_enabled(false); @@ -132,7 +132,7 @@ d->btnPlayerType->set_visible(true); d->btnPlayerType->set_enabled(stateaccess); - if (player.state == PlayerSettings::stateOpen) { + if (player.state == PlayerSettings::State::kOpen) { d->btnPlayerType->set_title(_("Open")); d->btnPlayerTeam->set_visible(false); d->btnPlayerTeam->set_visible(false); @@ -144,7 +144,7 @@ } else { std::string title; - if (player.state == PlayerSettings::stateComputer) { + if (player.state == PlayerSettings::State::kComputer) { if (player.ai.empty()) title = _("Computer"); else { @@ -156,7 +156,7 @@ title = _(impl->descname); } } - } else { // PlayerSettings::stateHuman + } else { // PlayerSettings::State::stateHuman title = _("Human"); } d->btnPlayerType->set_title(title); @@ -215,11 +215,11 @@ return; if (on) { - if (settings.players[d->plnum].state == PlayerSettings::stateClosed) + if (settings.players[d->plnum].state == PlayerSettings::State::kClosed) d->settings->next_player_state(d->plnum); } else { - if (settings.players[d->plnum].state != PlayerSettings::stateClosed) - d->settings->set_player_state(d->plnum, PlayerSettings::stateClosed); + if (settings.players[d->plnum].state != PlayerSettings::State::kClosed) + d->settings->set_player_state(d->plnum, PlayerSettings::State::kClosed); } }
_______________________________________________ Mailing list: https://launchpad.net/~widelands-dev Post to : widelands-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~widelands-dev More help : https://help.launchpad.net/ListHelp