widelands-dev team mailing list archive
-
widelands-dev team
-
Mailing list archive
-
Message #13829
Re: [Merge] lp:~widelands-dev/widelands/bug-1691339-editor-8-players into lp:widelands
Questions answered in-line. Will look into the positioning issue.
Diff comments:
>
> === modified file 'src/editor/ui_menus/player_menu.cc'
> --- src/editor/ui_menus/player_menu.cc 2018-05-22 11:29:41 +0000
> +++ src/editor/ui_menus/player_menu.cc 2018-07-08 12:09:41 +0000
> @@ -19,266 +19,313 @@
>
> #include "editor/ui_menus/player_menu.h"
>
> +#include <memory>
> +
> +#include <boost/algorithm/string.hpp>
> #include <boost/format.hpp>
>
> #include "base/i18n.h"
> -#include "base/wexception.h"
> #include "editor/editorinteractive.h"
> #include "editor/tools/set_starting_pos_tool.h"
> #include "graphic/graphic.h"
> +#include "graphic/playercolor.h"
> #include "logic/map.h"
> #include "logic/map_objects/tribes/tribe_basic_info.h"
> -#include "logic/player.h"
> -#include "ui_basic/editbox.h"
> -#include "ui_basic/messagebox.h"
> -#include "ui_basic/textarea.h"
> -
> -#define UNDEFINED_TRIBE_NAME "<undefined>"
> +#include "logic/widelands.h"
> +#include "profile/profile.h"
> +#include "ui_basic/checkbox.h"
> +#include "ui_basic/multilinetextarea.h"
> +
> +namespace {
> +constexpr int kMargin = 4;
> +// If this ever gets changed, don't forget to change the strings in the warning box as well.
> +constexpr Widelands::PlayerNumber max_recommended_players = 8;
> +} // namespace
> +
> +class EditorPlayerMenuWarningBox : public UI::Window {
> +public:
> + explicit EditorPlayerMenuWarningBox(UI::Panel* parent)
> + /** TRANSLATORS: Window title in the editor when a player has selected more than the recommended number of players */
> + : Window(parent, "editor_player_menu_warning_box", 0, 0, 500, 220, _("Too Many Players")),
> + box_(this, 0, 0, UI::Box::Vertical, 0, 0, 2 * kMargin),
> + warning_label_(&box_,
> + 0,
> + 0,
> + 300,
> + 0,
> + UI::PanelStyle::kWui,
> + /** TRANSLATORS: Info text in editor player menu when a player has selected more than the recommended number of players. Choice is made by OK/Abort. */
> + _("We do not recommend setting more than 8 players except for testing "
> + "purposes. Are you sure that you want more than 8 players?"),
> + UI::Align::kLeft,
> + UI::MultilineTextarea::ScrollMode::kNoScrolling),
> + /** TRANSLATORS: Checkbox for: 'We do not recommend setting more than 8 players except for
> + testing purposes. Are you sure that you want more than 8 players?' */
> + reminder_choice_(&box_, Vector2i::zero(), _("Do not remind me again")),
> + button_box_(&box_, kMargin, kMargin, UI::Box::Horizontal, 0, 0, 2 * kMargin),
> + ok_(&button_box_, "ok", 0, 0, 120, 0, UI::ButtonStyle::kWuiPrimary, _("OK")),
> + cancel_(&button_box_, "cancel", 0, 0, 120, 0, UI::ButtonStyle::kWuiSecondary, _("Abort")) {
> +
> + set_center_panel(&box_);
> +
> + box_.add(&warning_label_, UI::Box::Resizing::kFullSize);
> + box_.add(&reminder_choice_, UI::Box::Resizing::kFullSize);
> +
> + button_box_.add_inf_space();
> + button_box_.add(&cancel_);
> + button_box_.add_inf_space();
> + button_box_.add(&ok_);
> + button_box_.add_inf_space();
> + box_.add_space(kMargin);
> + box_.add(&button_box_, UI::Box::Resizing::kFullSize);
> + box_.add_space(kMargin);
> +
> + ok_.sigclicked.connect(boost::bind(&EditorPlayerMenuWarningBox::ok, boost::ref(*this)));
> + cancel_.sigclicked.connect(
> + boost::bind(&EditorPlayerMenuWarningBox::cancel, boost::ref(*this)));
> + }
> +
> + void ok() {
> + write_option();
> + end_modal<UI::Panel::Returncodes>(UI::Panel::Returncodes::kOk);
> + }
> +
> + void cancel() {
> + write_option();
> + end_modal<UI::Panel::Returncodes>(UI::Panel::Returncodes::kBack);
> + }
> +
> + void write_option() {
> + if (reminder_choice_.get_state()) {
> + g_options.pull_section("global").set_bool(
> + "editor_player_menu_warn_too_many_players", false);
> + }
> + }
> +
> +private:
> + UI::Box box_;
> + UI::MultilineTextarea warning_label_;
> + UI::Checkbox reminder_choice_;
> + UI::Box button_box_;
> + UI::Button ok_;
> + UI::Button cancel_;
> +};
>
> inline EditorInteractive& EditorPlayerMenu::eia() {
> return dynamic_cast<EditorInteractive&>(*get_parent());
> }
>
> EditorPlayerMenu::EditorPlayerMenu(EditorInteractive& parent, UI::UniqueWindow::Registry& registry)
> - : UI::UniqueWindow(&parent, "players_menu", ®istry, 340, 400, _("Player Options")),
> - add_player_(this,
> - "add_player",
> - get_inner_w() - 5 - 20,
> - 5,
> - 20,
> - 20,
> - UI::ButtonStyle::kWuiSecondary,
> - g_gr->images().get("images/ui_basic/scrollbar_up.png"),
> - _("Add player")),
> - remove_last_player_(this,
> - "remove_last_player",
> - 5,
> - 5,
> - 20,
> - 20,
> - UI::ButtonStyle::kWuiSecondary,
> - g_gr->images().get("images/ui_basic/scrollbar_down.png"),
> - _("Remove last player")),
> - tribenames_(Widelands::get_all_tribenames()) {
> - add_player_.set_enabled(parent.egbase().map().get_nrplayers() < kMaxPlayers);
> - add_player_.sigclicked.connect(
> - boost::bind(&EditorPlayerMenu::clicked_add_player, boost::ref(*this)));
> - remove_last_player_.sigclicked.connect(
> - boost::bind(&EditorPlayerMenu::clicked_remove_last_player, boost::ref(*this)));
> -
> - int32_t const spacing = 5;
> - int32_t const width = 20;
> - int32_t posy = 0;
> -
> - set_inner_size(375, 135);
> -
> - UI::Textarea* ta = new UI::Textarea(this, 0, 0, _("Number of Players"));
> - ta->set_pos(Vector2i((get_inner_w() - ta->get_w()) / 2, posy + 5));
> - posy += spacing + width;
> -
> - nr_of_players_ta_ = new UI::Textarea(this, 0, 0, "5");
> - nr_of_players_ta_->set_pos(Vector2i((get_inner_w() - nr_of_players_ta_->get_w()) / 2, posy + 5));
> -
> - posy += width + spacing + spacing;
> -
> - posy_ = posy;
> -
> - for (Widelands::PlayerNumber i = 0; i < kMaxPlayers; ++i) {
> - plr_names_[i] = nullptr;
> - plr_set_pos_buts_[i] = nullptr;
> - plr_set_tribes_buts_[i] = nullptr;
> - }
> -
> - if (parent.egbase().map().get_nrplayers() < 1) {
> - clicked_add_player();
> - }
> - update();
> -
> - set_thinks(true);
> -}
> -
> -/**
> - * Think function. Some things may change while this window
> - * is open
> - */
> -void EditorPlayerMenu::think() {
> - update();
> -}
> -
> -/**
> - * Update all
> -*/
> -void EditorPlayerMenu::update() {
> - if (is_minimal())
> - return;
> -
> - Widelands::Map* map = eia().egbase().mutable_map();
> - Widelands::PlayerNumber const nr_players = map->get_nrplayers();
> - {
> - assert(nr_players <= 99); // 2 decimal digits
> - char text[3];
> - if (char const nr_players_10 = nr_players / 10) {
> - text[0] = '0' + nr_players_10;
> - text[1] = '0' + nr_players % 10;
> - text[2] = '\0';
> - } else {
> - text[0] = '0' + nr_players;
> - text[1] = '\0';
> - }
> - nr_of_players_ta_->set_text(text);
> - }
> -
> - // Now remove all the unneeded stuff.
> - for (Widelands::PlayerNumber i = nr_players; i < kMaxPlayers; ++i) {
> - delete plr_names_[i];
> - plr_names_[i] = nullptr;
> - delete plr_set_pos_buts_[i];
> - plr_set_pos_buts_[i] = nullptr;
> - delete plr_set_tribes_buts_[i];
> - plr_set_tribes_buts_[i] = nullptr;
> - }
> - int32_t posy = posy_;
> - int32_t const spacing = 5;
> - int32_t const size = 20;
> -
> - iterate_player_numbers(p, nr_players) {
> - int32_t posx = spacing;
> - if (!plr_names_[p - 1]) {
> - plr_names_[p - 1] = new UI::EditBox(this, posx, posy, 140, size, 2, UI::PanelStyle::kWui);
> - plr_names_[p - 1]->changed.connect(
> - boost::bind(&EditorPlayerMenu::name_changed, this, p - 1));
> - posx += 140 + spacing;
> - plr_names_[p - 1]->set_text(map->get_scenario_player_name(p));
> - }
> -
> - if (!plr_set_tribes_buts_[p - 1]) {
> - plr_set_tribes_buts_[p - 1] = new UI::Button(
> - this, "tribe", posx, posy, 140, size, UI::ButtonStyle::kWuiSecondary, "");
> - plr_set_tribes_buts_[p - 1]->sigclicked.connect(
> - boost::bind(&EditorPlayerMenu::player_tribe_clicked, boost::ref(*this), p - 1));
> - posx += 140 + spacing;
> - }
> -
> - // Get/Set (localized) tribe names
> - if (map->get_scenario_player_tribe(p) != UNDEFINED_TRIBE_NAME) {
> - selected_tribes_[p - 1] = map->get_scenario_player_tribe(p);
> - } else {
> - selected_tribes_[p - 1] = tribenames_[0];
> - map->set_scenario_player_tribe(p, selected_tribes_[p - 1]);
> - }
> -
> - plr_set_tribes_buts_[p - 1]->set_title(
> - Widelands::get_tribeinfo(selected_tribes_[p - 1]).descname);
> -
> - // Set default AI and closeable to false (always default - should be changed by hand)
> - map->set_scenario_player_ai(p, "");
> - map->set_scenario_player_closeable(p, false);
> -
> - // Set Starting pos button.
> - if (!plr_set_pos_buts_[p - 1]) {
> - plr_set_pos_buts_[p - 1] = new UI::Button(this, "starting_pos", posx, posy, size, size,
> - UI::ButtonStyle::kWuiSecondary, nullptr, "");
> - plr_set_pos_buts_[p - 1]->sigclicked.connect(
> - boost::bind(&EditorPlayerMenu::set_starting_pos_clicked, boost::ref(*this), p));
> - }
> + : UI::UniqueWindow(&parent, "players_menu", ®istry, 100, 100, _("Player Options")),
> + box_(this, kMargin, kMargin, UI::Box::Vertical),
> + no_of_players_(&box_,
> + 0,
> + 0,
> + 50,
> + 100,
> + 24,
> + _("Number of players"),
> + UI::DropdownType::kTextual,
> + UI::PanelStyle::kWui),
> + default_tribe_(Widelands::get_all_tribenames().front()) {
> + box_.set_size(100, 100); // Prevent assert failures
> + box_.add(&no_of_players_, UI::Box::Resizing::kFullSize);
> + box_.add_space(2 * kMargin);
> +
> + const Widelands::Map& map = eia().egbase().map();
> +
> + // Ensure that there is at least 1 player
> + if (map.get_nrplayers() < 1) {
> + Widelands::Map* mutable_map = eia().egbase().mutable_map();
> + mutable_map->set_nrplayers(1);
> + // Init player 1
> + mutable_map->set_scenario_player_ai(1, "");
> + mutable_map->set_scenario_player_closeable(1, false);
> + /** TRANSLATORS: Default player name, e.g. Player 1 */
> + mutable_map->set_scenario_player_name(1, (boost::format(_("Player %u")) % 1).str());
> + mutable_map->set_scenario_player_tribe(1, default_tribe_);
> + eia().set_need_save(true);
> + }
> +
> + const Widelands::PlayerNumber nr_players = map.get_nrplayers();
> + iterate_player_numbers(p, kMaxPlayers) {
> + const bool map_has_player = p <= nr_players;
> +
> + no_of_players_.add(boost::lexical_cast<std::string>(static_cast<unsigned int>(p)), p);
> + no_of_players_.selected.connect(
> + boost::bind(&EditorPlayerMenu::no_of_players_clicked, boost::ref(*this)));
> +
> + UI::Box* row = new UI::Box(&box_, 0, 0, UI::Box::Horizontal);
> +
> + // Name
> + UI::EditBox* plr_name = new UI::EditBox(row, 0, 0, 0, 0, kMargin, UI::PanelStyle::kWui);
> + if (map_has_player) {
> + plr_name->set_text(map.get_scenario_player_name(p));
> + }
> + plr_name->changed.connect(boost::bind(&EditorPlayerMenu::name_changed, this, p - 1));
> + row->add(plr_name, UI::Box::Resizing::kFillSpace);
> + row->add_space(kMargin);
> +
> + // Tribe
> + UI::Dropdown<std::string>* plr_tribe =
> + new UI::Dropdown<std::string>(row, 0, 0, 50, 400, plr_name->get_h(), _("Tribe"),
> + UI::DropdownType::kPictorial, UI::PanelStyle::kWui);
> + {
> + i18n::Textdomain td("tribes");
> + for (const Widelands::TribeBasicInfo& tribeinfo : Widelands::get_all_tribeinfos()) {
> + plr_tribe->add(_(tribeinfo.descname), tribeinfo.name,
> + g_gr->images().get(tribeinfo.icon), false, tribeinfo.tooltip);
> + }
> + }
> + const std::string player_scenario_tribe =
> + map_has_player ? map.get_scenario_player_tribe(p) : default_tribe_;
> + plr_tribe->select(Widelands::tribe_exists(player_scenario_tribe) ? player_scenario_tribe :
> + default_tribe_);
> + plr_tribe->selected.connect(
> + boost::bind(&EditorPlayerMenu::player_tribe_clicked, boost::ref(*this), p - 1));
> + row->add(plr_tribe);
> + row->add_space(kMargin);
> +
> + // Starting position
> const Image* player_image =
> playercolor_image(p - 1, "images/players/player_position_menu.png");
> assert(player_image);
>
> - plr_set_pos_buts_[p - 1]->set_pic(player_image);
> - posy += size + spacing;
> - }
> - add_player_.set_enabled(nr_players < kMaxPlayers);
> - remove_last_player_.set_enabled(1 < nr_players);
> - set_inner_size(get_inner_w(), posy + spacing);
> -}
> -
> -void EditorPlayerMenu::clicked_add_player() {
> - Widelands::Map* map = eia().egbase().mutable_map();
> - Widelands::PlayerNumber const nr_players = map->get_nrplayers() + 1;
> - assert(nr_players <= kMaxPlayers);
> - map->set_nrplayers(nr_players);
> - { // register new default name for this players
> - assert(nr_players <= 99); // 2 decimal digits
> - const std::string name =
> - /** TRANSLATORS: Default player name, e.g. Player 1 */
> - (boost::format(_("Player %u")) % static_cast<unsigned int>(nr_players)).str();
> - map->set_scenario_player_name(nr_players, name);
> - }
> - map->set_scenario_player_tribe(nr_players, tribenames_[0]);
> - eia().set_need_save(true);
> - add_player_.set_enabled(nr_players < kMaxPlayers);
> - remove_last_player_.set_enabled(true);
> - update();
> -}
> -
> -void EditorPlayerMenu::clicked_remove_last_player() {
> + UI::Button* plr_position =
> + new UI::Button(row, "tribe", 0, 0, plr_tribe->get_h(), plr_tribe->get_h(),
> + UI::ButtonStyle::kWuiSecondary, player_image);
> + plr_position->sigclicked.connect(
> + boost::bind(&EditorPlayerMenu::set_starting_pos_clicked, boost::ref(*this), p));
> + row->add(plr_position);
> +
> + box_.add(row, UI::Box::Resizing::kFullSize);
> + box_.add_space(kMargin);
> + row->set_visible(map_has_player);
> +
> + rows_.push_back(
> + std::unique_ptr<PlayerEditRow>(new PlayerEditRow(row, plr_name, plr_position, plr_tribe)));
> + }
> +
> + // Make room for 8 players
> + no_of_players_.set_max_items(max_recommended_players);
> + no_of_players_.select(nr_players);
> +
> + // Init button states
> + set_starting_pos_clicked(1);
> + layout();
> +}
> +
> +void EditorPlayerMenu::layout() {
> + assert(!rows_.empty());
> + const Widelands::PlayerNumber nr_players = eia().egbase().map().get_nrplayers();
> + box_.set_size(310, no_of_players_.get_h() + kMargin +
> + nr_players * (rows_.front()->tribe->get_h() + kMargin));
> + set_inner_size(box_.get_w() + 2 * kMargin, box_.get_h() + 2 * kMargin);
> +}
> +
> +void EditorPlayerMenu::no_of_players_clicked() {
> EditorInteractive& menu = eia();
> Widelands::Map* map = menu.egbase().mutable_map();
> Widelands::PlayerNumber const old_nr_players = map->get_nrplayers();
> - Widelands::PlayerNumber const nr_players = old_nr_players - 1;
> + Widelands::PlayerNumber const nr_players = no_of_players_.get_selected();
> assert(1 <= nr_players);
> -
> - // if removed player was selected switch to the next highest player
> - if (old_nr_players == menu.tools()->set_starting_pos.get_current_player()) {
> - set_starting_pos_clicked(nr_players);
> - }
> -
> - map->set_nrplayers(nr_players);
> - add_player_.set_enabled(nr_players < kMaxPlayers);
> - remove_last_player_.set_enabled(1 < nr_players);
> - update();
> + assert(nr_players <= kMaxPlayers);
> +
> + if (old_nr_players == nr_players) {
> + return;
> + }
> +
> + // Display a warning if there are too many players
> + if (nr_players > max_recommended_players) {
> + if (g_options.pull_section("global").get_bool(
> + "editor_player_menu_warn_too_many_players", true)) {
> + EditorPlayerMenuWarningBox warning(get_parent());
We allow 16 players in maps so that Tibor can create the AI test maps that he needs. In general, there should be no maps with more than 8 players though - the game loading UI doesn't support it, and we can expect performance problems.
> + if (warning.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kBack) {
> + // Abort setting of players
> + no_of_players_.select(std::min(old_nr_players, max_recommended_players));
> + }
> + }
> + }
> +
> + if (old_nr_players < nr_players) {
> + // Add new players
> + map->set_nrplayers(nr_players);
> +
> + for (Widelands::PlayerNumber pn = old_nr_players + 1; pn <= nr_players; ++pn) {
> + map->set_scenario_player_ai(pn, "");
> + map->set_scenario_player_closeable(pn, false);
> +
> + // Register new default name and tribe for these players
> + const std::string name =
> + /** TRANSLATORS: Default player name, e.g. Player 1 */
> + (boost::format(_("Player %u")) % static_cast<unsigned int>(pn)).str();
> + map->set_scenario_player_name(pn, name);
> + rows_.at(pn - 1)->name->set_text(name);
> +
> + const std::string& tribename = rows_.at(pn - 1)->tribe->get_selected();
> + assert(Widelands::tribe_exists(tribename));
> + map->set_scenario_player_tribe(pn, tribename);
> + rows_.at(pn - 1)->box->set_visible(true);
> + }
> + // Update button states
> + set_starting_pos_clicked(menu.tools()->set_starting_pos.get_current_player());
> + } else {
> + // If a removed player was selected, switch starting pos tool to the highest available player
> + if (old_nr_players >= menu.tools()->set_starting_pos.get_current_player()) {
> + set_starting_pos_clicked(nr_players);
> + }
> +
> + // Hide extra players
> + map->set_nrplayers(nr_players);
> + for (Widelands::PlayerNumber pn = nr_players; pn < old_nr_players; ++pn) {
> + rows_.at(pn)->box->set_visible(false);
> + }
> + }
> + menu.set_need_save(true);
> + layout();
> }
>
> -/**
> - * Player Tribe Button clicked
> - */
> -void EditorPlayerMenu::player_tribe_clicked(uint8_t n) {
> +void EditorPlayerMenu::player_tribe_clicked(size_t row) {
> + const std::string& tribename = rows_.at(row)->tribe->get_selected();
> + assert(Widelands::tribe_exists(tribename));
> EditorInteractive& menu = eia();
> - if (!Widelands::tribe_exists(selected_tribes_[n])) {
> - throw wexception("Map defines tribe %s, but it does not exist!", selected_tribes_[n].c_str());
> - }
> - uint32_t i;
> - for (i = 0; i < tribenames_.size(); ++i) {
> - if (tribenames_[i] == selected_tribes_[n]) {
> - break;
> - }
> - }
> - selected_tribes_[n] = i == tribenames_.size() - 1 ? tribenames_[0] : tribenames_[++i];
> - menu.egbase().mutable_map()->set_scenario_player_tribe(n + 1, selected_tribes_[n]);
> + menu.egbase().mutable_map()->set_scenario_player_tribe(row + 1, tribename);
> menu.set_need_save(true);
> - update();
> }
>
> -/**
> - * Set Current Start Position button selected
> - */
> -void EditorPlayerMenu::set_starting_pos_clicked(uint8_t n) {
> +void EditorPlayerMenu::set_starting_pos_clicked(size_t row) {
> EditorInteractive& menu = eia();
> // jump to the current node
> Widelands::Map* map = menu.egbase().mutable_map();
> - if (Widelands::Coords const sp = map->get_starting_pos(n)) {
> + if (Widelands::Coords const sp = map->get_starting_pos(row)) {
> menu.map_view()->scroll_to_field(sp, MapView::Transition::Smooth);
> }
>
> // select tool set mplayer
> menu.select_tool(menu.tools()->set_starting_pos, EditorTool::First);
> - menu.tools()->set_starting_pos.set_current_player(n);
> + menu.tools()->set_starting_pos.set_current_player(row);
>
> // reselect tool, so everything is in a defined state
> menu.select_tool(menu.tools()->current(), EditorTool::First);
> - update();
> +
> + // Signal player position states via button states
> + iterate_player_numbers(pn, map->get_nrplayers()) {
> + if (pn == row) {
> + rows_.at(pn - 1)->position->set_background_style(UI::ButtonStyle::kWuiPrimary);
> + rows_.at(pn - 1)->position->set_perm_pressed(true);
> + } else {
> + rows_.at(pn - 1)->position->set_background_style(UI::ButtonStyle::kWuiSecondary);
> + rows_.at(pn - 1)->position->set_perm_pressed(map->get_starting_pos(pn) !=
> + Widelands::Coords::null());
> + }
> + }
> }
>
> -/**
> - * Player name has changed
> - */
> -void EditorPlayerMenu::name_changed(int32_t m) {
> +void EditorPlayerMenu::name_changed(size_t row) {
> // Player name has been changed.
> - std::string text = plr_names_[m]->text();
> + const std::string& text = rows_.at(row)->name->text();
> EditorInteractive& menu = eia();
> Widelands::Map* map = menu.egbase().mutable_map();
> - map->set_scenario_player_name(m + 1, text);
> - plr_names_[m]->set_text(map->get_scenario_player_name(m + 1));
> + map->set_scenario_player_name(row + 1, text);
> menu.set_need_save(true);
> }
>
> === modified file 'src/ui_basic/dropdown.cc'
> --- src/ui_basic/dropdown.cc 2018-04-27 06:11:05 +0000
> +++ src/ui_basic/dropdown.cc 2018-07-08 12:09:41 +0000
> @@ -129,8 +129,13 @@
> }
>
> BaseDropdown::~BaseDropdown() {
> - // Listselect is already taking care of the cleanup,
> - // so no call to clear() needed here.
> + // The list needs to be able to drop outside of windows, so it won't close with the window.
> + // Deleting here leads to conflict with who gets to delete it, so we hide it instead.
> + // TODO(GunChleoc): Investigate whether we can find a better solution for this
We would need to completely reprogram the UI::Panel class with shared_ptr I guess - that would e a huge change.
Dropdowns need parents higher up than the actual parent, otherwise the list would be cut off if it's bigger than the parent Window - or, even worse, its parent Box.
> + if (list_) {
> + list_->clear();
> + list_->set_visible(false);
> + }
> }
>
> void BaseDropdown::set_height(int height) {
--
https://code.launchpad.net/~widelands-dev/widelands/bug-1691339-editor-8-players/+merge/349092
Your team Widelands Developers is subscribed to branch lp:~widelands-dev/widelands/bug-1691339-editor-8-players.
References