diff --git a/data/_main.cfg b/data/_main.cfg index dfb615a4f2f..211d2ec7c63 100644 --- a/data/_main.cfg +++ b/data/_main.cfg @@ -112,6 +112,8 @@ {game_config.cfg} +{game_presets.cfg} + [textdomain] name="wesnoth-lib" [/textdomain] diff --git a/data/game_presets.cfg b/data/game_presets.cfg new file mode 100644 index 00000000000..a4fef06474c --- /dev/null +++ b/data/game_presets.cfg @@ -0,0 +1,38 @@ +[game_presets] + [game] + scenario = "multiplayer_The_Freelands" + era = "era_default" + fog = yes + shroud = no + village_gold = 2 + village_support = 1 + experience_modifier = 70 + countdown = false + random_start_time = no + shuffle_sides = no + [/game] + [game] + scenario = "multiplayer_The_Wilderlands" + era = "era_default" + fog = yes + shroud = no + village_gold = 2 + village_support = 1 + experience_modifier = 70 + countdown = false + random_start_time = no + shuffle_sides = no + [/game] + [game] + scenario = "multiplayer_2p_Dark_Forecast" + era = "era_default" + fog = yes + shroud = no + village_gold = 2 + village_support = 1 + experience_modifier = 100 + countdown = false + random_start_time = no + shuffle_sides = no + [/game] +[/game_presets] diff --git a/data/schema/game_config.cfg b/data/schema/game_config.cfg index c09835852cc..38f2f3b6dfb 100644 --- a/data/schema/game_config.cfg +++ b/data/schema/game_config.cfg @@ -524,5 +524,28 @@ {SIMPLE_KEY code string} {DATA_TAG args 0 1 any} [/tag] + [tag] + name="game_presets" + max=infinite + [tag] + name="game" + max=infinite + {SIMPLE_KEY scenario string} + {SIMPLE_KEY era string} + {SIMPLE_KEY fog bool} + {SIMPLE_KEY shroud bool} + {SIMPLE_KEY village_gold int} + {SIMPLE_KEY village_support int} + {SIMPLE_KEY experience_modifier int} + {SIMPLE_KEY countdown bool} + {SIMPLE_KEY countdown_init_time int} + {SIMPLE_KEY countdown_turn_bonus int} + {SIMPLE_KEY countdown_reservoir_time int} + {SIMPLE_KEY countdown_action_bonus int} + {SIMPLE_KEY random_start_time bool} + {SIMPLE_KEY shuffle_sides bool} + {SIMPLE_KEY turn_count int} + [/tag] + [/tag] [/tag] [/wml_schema] diff --git a/src/game_initialization/connect_engine.cpp b/src/game_initialization/connect_engine.cpp index 98cc4758da6..aa10d31d6cc 100644 --- a/src/game_initialization/connect_engine.cpp +++ b/src/game_initialization/connect_engine.cpp @@ -741,7 +741,9 @@ void connect_engine::send_level_data() const "name", params_.name, "password", params_.password, "ignored", prefs::get().get_ignored_delim(), - "auto_hosted", false, + // all queue games count as auto hosted, but not all auto hosted games are queue games + "auto_hosted", mp_metadata_ ? mp_metadata_->is_queue_game : false, + "queue_game", mp_metadata_ ? mp_metadata_->is_queue_game : false, }, }); mp::send_to_server(level_); diff --git a/src/game_initialization/create_engine.cpp b/src/game_initialization/create_engine.cpp index a11f921e782..7c9f50b04c2 100644 --- a/src/game_initialization/create_engine.cpp +++ b/src/game_initialization/create_engine.cpp @@ -545,6 +545,15 @@ void create_engine::set_current_era_index(const std::size_t index, bool force) dependency_manager_->try_era_by_index(index, force); } +void create_engine::set_current_era_id(const std::string& id, bool force) +{ + std::size_t index = dependency_manager_->get_era_index(id); + + current_era_index_ = index; + + dependency_manager_->try_era_by_index(index, force); +} + bool create_engine::toggle_mod(const std::string& id, bool force) { force |= state_.classification().type != campaign_type::type::multiplayer; diff --git a/src/game_initialization/create_engine.hpp b/src/game_initialization/create_engine.hpp index d9b2d6bd273..c885813b8e0 100644 --- a/src/game_initialization/create_engine.hpp +++ b/src/game_initialization/create_engine.hpp @@ -356,6 +356,7 @@ public: void set_current_level(const std::size_t index); void set_current_era_index(const std::size_t index, bool force = false); + void set_current_era_id(const std::string& id, bool force = false); std::size_t current_era_index() const { @@ -401,8 +402,6 @@ private: void init_extras(const MP_EXTRA extra_type); void apply_level_filters(); - std::size_t map_level_index(std::size_t index) const; - level_type::type current_level_type_; std::size_t current_level_index_; diff --git a/src/game_initialization/depcheck.cpp b/src/game_initialization/depcheck.cpp index b10e6c318ec..d68bda422dc 100644 --- a/src/game_initialization/depcheck.cpp +++ b/src/game_initialization/depcheck.cpp @@ -409,6 +409,20 @@ int manager::get_era_index() const return -1; } +int manager::get_era_index(const std::string& id) const +{ + int result = 0; + for(const config& i : depinfo_.child_range("era")) { + if(i["id"] == id) { + return result; + } + + result++; + } + + return -1; +} + int manager::get_scenario_index() const { int result = 0; diff --git a/src/game_initialization/depcheck.hpp b/src/game_initialization/depcheck.hpp index e1dc5228702..1591bebddbb 100644 --- a/src/game_initialization/depcheck.hpp +++ b/src/game_initialization/depcheck.hpp @@ -146,6 +146,7 @@ public: * @return the index of the era */ int get_era_index() const; + int get_era_index(const std::string& id) const; /** * Returns the selected scenario diff --git a/src/game_initialization/lobby_data.cpp b/src/game_initialization/lobby_data.cpp index 61055913566..0dae531657c 100644 --- a/src/game_initialization/lobby_data.cpp +++ b/src/game_initialization/lobby_data.cpp @@ -114,6 +114,7 @@ game_info::game_info(const config& game, const std::vector& install , map_data(game["map_data"]) , name(font::escape_text(game["name"].str())) , scenario() + , scenario_id() , type_marker() , remote_scenario(false) , map_info() @@ -261,6 +262,7 @@ game_info::game_info(const config& game, const std::vector& install if(level_cfg) { type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false); scenario = (*level_cfg)["name"].str(); + scenario_id = (*level_cfg)["id"].str(); info_stream << scenario; // Reloaded games do not match the original scenario hash, so it makes no sense diff --git a/src/game_initialization/lobby_data.hpp b/src/game_initialization/lobby_data.hpp index 0522743093c..f3b2245ed2a 100644 --- a/src/game_initialization/lobby_data.hpp +++ b/src/game_initialization/lobby_data.hpp @@ -70,6 +70,7 @@ struct game_info std::string map_data; std::string name; std::string scenario; + std::string scenario_id; std::string type_marker; bool remote_scenario; std::string map_info; diff --git a/src/game_initialization/lobby_info.cpp b/src/game_initialization/lobby_info.cpp index 5ec5d768f9f..a60d240e63f 100644 --- a/src/game_initialization/lobby_info.cpp +++ b/src/game_initialization/lobby_info.cpp @@ -16,6 +16,7 @@ #include "game_initialization/lobby_info.hpp" #include "addon/manager.hpp" // for installed_addons +#include "game_config_manager.hpp" #include "log.hpp" #include "mp_ui_alerts.hpp" @@ -120,6 +121,61 @@ void lobby_info::process_gamelist(const config& data) games_by_id_.clear(); + int queued_id = 0; + for(const config& game : game_config_manager::get()->game_config().mandatory_child("game_presets").child_range("game")) { + config qgame; + const config& scenario = game_config_manager::get()->game_config().find_mandatory_child("multiplayer", "id", game["scenario"].str()); + int human_sides = 0; + for(const auto& side : scenario.child_range("side")) { + if(side["controller"].str() == "human") { + human_sides++; + } + } + if(human_sides == 0) { + ERR_LB << "No human sides for scenario " << game["scenario"]; + continue; + } + // negative id means a queue-defined game + queued_id--; + qgame["id"] = queued_id; + // all are set as auto_hosted so they show up in that tab of the MP lobby + qgame["auto_hosted"] = true; + + qgame["name"] = scenario["name"]; + qgame["mp_scenario"] = game["scenario"]; + qgame["mp_era"] = game["era"]; + qgame["mp_use_map_settings"] = game["use_map_settings"]; + qgame["mp_fog"] = game["fog"]; + qgame["mp_shroud"] = game["shroud"]; + qgame["mp_village_gold"] = game["village_gold"]; + qgame["experience_modifier"] = game["experience_modifier"]; + + qgame["mp_countdown"] = game["countdown"]; + if(qgame["countdown"].to_bool()) { + qgame["mp_countdown_reservoir_time"] = game["countdown_reservoir_time"]; + qgame["mp_countdown_init_time"] = game["countdown_init_time"]; + qgame["mp_countdown_action_bonus"] = game["countdown_action_bonus"]; + qgame["mp_countdown_turn_bonus"] = game["countdown_turn_bonus"]; + } + + qgame["observer"] = game["observer"]; + qgame["human_sides"] = human_sides; + + if(scenario.has_attribute("map_data")) { + qgame["map_data"] = scenario["map_data"]; + } else { + qgame["map_data"] = filesystem::read_map(scenario["map_file"]); + } + qgame["hash"] = game_config_manager::get()->game_config().mandatory_child("multiplayer_hashes")[game["scenario"].str()]; + + config& qchild = qgame.add_child("slot_data"); + qchild["vacant"] = human_sides; + qchild["max"] = human_sides; + + game_info g(qgame, installed_addons_); + games_by_id_.emplace(g.id, std::move(g)); + } + for(const auto& c : gamelist_.mandatory_child("gamelist").child_range("game")) { game_info game(c, installed_addons_); games_by_id_.emplace(game.id, std::move(game)); diff --git a/src/game_initialization/multiplayer.cpp b/src/game_initialization/multiplayer.cpp index 8a5b2944cc4..cb67bb5c5e1 100644 --- a/src/game_initialization/multiplayer.cpp +++ b/src/game_initialization/multiplayer.cpp @@ -118,11 +118,14 @@ private: /** Opens the MP lobby. */ bool enter_lobby_mode(); - /** Opens the MP Create screen for hosts to configure a new game. */ - void enter_create_mode(); + /** + * Opens the MP Create screen for hosts to configure a new game. + * @param preset_scenario contains a scenario id if present + */ + void enter_create_mode(utils::optional preset_scenario = utils::nullopt); /** Opens the MP Staging screen for hosts to wait for players. */ - void enter_staging_mode(); + void enter_staging_mode(bool preset); /** Opens the MP Join Game screen for non-host players and observers. */ void enter_wait_mode(int game_id, bool observe); @@ -536,14 +539,19 @@ bool mp_manager::enter_lobby_mode() int dlg_retval = 0; int dlg_joined_game_id = 0; + std::string preset_scenario = ""; { gui2::dialogs::mp_lobby dlg(lobby_info, *connection, dlg_joined_game_id); dlg.show(); dlg_retval = dlg.get_retval(); + preset_scenario = dlg.queue_game_scenario_id(); } try { switch(dlg_retval) { + case gui2::dialogs::mp_lobby::CREATE_PRESET: + enter_create_mode(utils::make_optional(preset_scenario)); + break; case gui2::dialogs::mp_lobby::CREATE: enter_create_mode(); break; @@ -572,18 +580,26 @@ bool mp_manager::enter_lobby_mode() return true; } -void mp_manager::enter_create_mode() +void mp_manager::enter_create_mode(utils::optional preset_scenario) { DBG_MP << "entering create mode"; - if(gui2::dialogs::mp_create_game::execute(state, connection == nullptr)) { - enter_staging_mode(); + if(preset_scenario) { + for(const config& game : game_config_manager::get()->game_config().mandatory_child("game_presets").child_range("game")) { + if(game["scenario"].str() == preset_scenario.value()) { + gui2::dialogs::mp_create_game::quick_mp_setup(state, game); + enter_staging_mode(true); + break; + } + } + } else if(gui2::dialogs::mp_create_game::execute(state, connection == nullptr)) { + enter_staging_mode(false); } else if(connection) { connection->send_data(config("refresh_lobby")); } } -void mp_manager::enter_staging_mode() +void mp_manager::enter_staging_mode(bool preset) { DBG_MP << "entering connect mode"; @@ -594,6 +610,7 @@ void mp_manager::enter_staging_mode() metadata = std::make_unique(*connection); metadata->connected_players.insert(prefs::get().login()); metadata->is_host = true; + metadata->is_queue_game = preset; } bool dlg_ok = false; diff --git a/src/game_initialization/playcampaign.hpp b/src/game_initialization/playcampaign.hpp index 1c2c583fcc1..dcf912f8627 100644 --- a/src/game_initialization/playcampaign.hpp +++ b/src/game_initialization/playcampaign.hpp @@ -33,6 +33,7 @@ struct mp_game_metadata , skip_replay(false) , skip_replay_blindfolded(false) , connection(wdc) + , is_queue_game(false) { } @@ -43,6 +44,7 @@ struct mp_game_metadata bool skip_replay; bool skip_replay_blindfolded; wesnothd_connection& connection; + bool is_queue_game; }; class campaign_controller diff --git a/src/gui/dialogs/multiplayer/lobby.cpp b/src/gui/dialogs/multiplayer/lobby.cpp index 9de573bc49c..778343e479a 100644 --- a/src/gui/dialogs/multiplayer/lobby.cpp +++ b/src/gui/dialogs/multiplayer/lobby.cpp @@ -756,6 +756,13 @@ void mp_lobby::process_network_data(const config& data) announcements_ = info["message"].str(); return; } + } else if(auto create = data.optional_child("create_game")) { + queue_game_scenario_id_ = create["mp_scenario"]; + set_retval(CREATE_PRESET); + return; + } else if(auto join_game = data.optional_child("join_game")) { + enter_game_by_id(join_game["id"].to_int(), JOIN_MODE::DO_JOIN); + return; } chatbox_->process_network_data(data); @@ -870,11 +877,15 @@ void mp_lobby::enter_game(const mp::game_info& game, JOIN_MODE mode) join_data["password"] = password; } + join_data["mp_scenario"] = game.scenario_id; mp::send_to_server(response); - joined_game_id_ = game.id; - // We're all good. Close lobby and proceed to game! - set_retval(try_join ? JOIN : OBSERVE); + if(game.id >= 0) { + joined_game_id_ = game.id; + + // We're all good. Close lobby and proceed to game! + set_retval(try_join ? JOIN : OBSERVE); + } } void mp_lobby::enter_game_by_index(const int index, JOIN_MODE mode) diff --git a/src/gui/dialogs/multiplayer/lobby.hpp b/src/gui/dialogs/multiplayer/lobby.hpp index 9d69c1633c1..9a0a047269f 100644 --- a/src/gui/dialogs/multiplayer/lobby.hpp +++ b/src/gui/dialogs/multiplayer/lobby.hpp @@ -61,10 +61,13 @@ public: QUIT, JOIN, OBSERVE, - CREATE, - RELOAD_CONFIG + CREATE, /** player clicked the Create button */ + RELOAD_CONFIG, + CREATE_PRESET /** player clicked Join button on an [mp_queue] game, but there was no existing game to join */ }; + const std::string queue_game_scenario_id() const { return queue_game_scenario_id_; }; + private: void update_selected_game(); @@ -174,6 +177,8 @@ private: int& joined_game_id_; + std::string queue_game_scenario_id_; + friend struct lobby_delay_gamelist_update_guard; static inline std::string server_information_ = ""; diff --git a/src/gui/dialogs/multiplayer/mp_create_game.cpp b/src/gui/dialogs/multiplayer/mp_create_game.cpp index fc293f0255e..329df6b479c 100644 --- a/src/gui/dialogs/multiplayer/mp_create_game.cpp +++ b/src/gui/dialogs/multiplayer/mp_create_game.cpp @@ -142,6 +142,85 @@ mp_create_game::mp_create_game(saved_game& state, bool local_mode) set_allow_plugin_skip(false); } +// NOLINTNEXTLINE(performance-unnecessary-value-param) +void mp_create_game::quick_mp_setup(saved_game& state, const config presets) +{ + // from constructor + ng::create_engine create(state); + create.init_active_mods(); + create.get_state().clear(); + create.get_state().classification().type = campaign_type::type::multiplayer; + + // from pre_show + create.set_current_level_type(level_type::type::scenario); + const auto& levels = create.get_levels_by_type(level_type::type::scenario); + for(std::size_t i = 0; i < levels.size(); i++) { + if(levels[i]->id() == presets["scenario"].str()) { + create.set_current_level(i); + } + } + + create.set_current_era_id(presets["era"]); + + // from post_show + create.prepare_for_era_and_mods(); + create.prepare_for_scenario(); + create.get_parameters(); + create.prepare_for_new_level(); + + mp_game_settings& params = create.get_state().mp_settings(); + params.use_map_settings = true; + params.num_turns = presets["turn_count"].to_int(-1); + params.village_gold = presets["village_gold"].to_int(); + params.village_support = presets["village_support"].to_int(); + params.xp_modifier = presets["experience_modifier"].to_int(); + params.random_start_time = presets["random_start_time"].to_bool(); + params.fog_game = presets["fog"].to_bool(); + params.shroud_game = presets["shroud"].to_bool(); + + // write to scenario + // queue games are supposed to all use the same settings, not be modified by the user + // can be removed later if we jump straight from the lobby into a game instead of going to the staging screen to wait for other players to join + config& scenario = create.get_state().get_starting_point(); + + if(params.random_start_time) { + if(!tod_manager::is_start_ToD(scenario["random_start_time"])) { + scenario["random_start_time"] = true; + } + } else { + scenario["random_start_time"] = false; + } + + scenario["experience_modifier"] = params.xp_modifier; + scenario["turns"] = params.num_turns; + + for(config& side : scenario.child_range("side")) { + side["controller_lock"] = true; + side["team_lock"] = true; + side["gold_lock"] = true; + side["income_lock"] = true; + + side["fog"] = params.fog_game; + side["shroud"] = params.shroud_game; + side["village_gold"] = params.village_gold; + side["village_support"] = params.village_support; + } + + params.mp_countdown = presets["countdown"].to_bool(); + params.mp_countdown_init_time = std::chrono::seconds{presets["countdown_init_time"].to_int()}; + params.mp_countdown_turn_bonus = std::chrono::seconds{presets["countdown_turn_bonus"].to_int()}; + params.mp_countdown_reservoir_time = std::chrono::seconds{presets["countdown_reservoir_time"].to_int()}; + params.mp_countdown_action_bonus = std::chrono::seconds{presets["countdown_action_bonus"].to_int()}; + + params.allow_observers = true; + params.private_replay = false; + create.get_state().classification().oos_debug = false; + params.shuffle_sides = presets["shuffle_sides"].to_bool(); + + params.mode = random_faction_mode::type::no_mirror; + params.name = settings::game_name_default(); +} + void mp_create_game::pre_show() { find_widget("game_name").set_value(local_mode_ ? "" : settings::game_name_default()); diff --git a/src/gui/dialogs/multiplayer/mp_create_game.hpp b/src/gui/dialogs/multiplayer/mp_create_game.hpp index ac39d5ca959..e4212d114cb 100644 --- a/src/gui/dialogs/multiplayer/mp_create_game.hpp +++ b/src/gui/dialogs/multiplayer/mp_create_game.hpp @@ -41,6 +41,12 @@ public: /** The execute function. See @ref modal_dialog for more information. */ DEFINE_SIMPLE_EXECUTE_WRAPPER(mp_create_game); + /** + * @a presets needs to be a copy! + * Otherwise you'll get segfaults when clicking the Join button since it results in the configs getting reloaded. + */ + static void quick_mp_setup(saved_game& state, const config presets); + private: virtual const std::string& window_id() const override; diff --git a/src/server/wesnothd/game.cpp b/src/server/wesnothd/game.cpp index e874326c14a..5990fa4fb8d 100644 --- a/src/server/wesnothd/game.cpp +++ b/src/server/wesnothd/game.cpp @@ -77,6 +77,7 @@ int game::db_id_num = 1; game::game(wesnothd::server& server, player_connections& player_connections, player_iterator host, + bool is_queue_game, const std::string& name, bool save_replays, const std::string& replay_save_path) @@ -112,6 +113,7 @@ game::game(wesnothd::server& server, player_connections& player_connections, , replay_save_path_(replay_save_path) , rng_() , last_choice_request_id_(-1) /* or maybe 0 ? it shouldn't matter*/ + , is_queue_game_(is_queue_game) { players_.push_back(owner_); @@ -148,6 +150,11 @@ static const simple_wml::node& get_multiplayer(const simple_wml::node& root) } } +const std::string game::get_scenario_id() const +{ + return get_multiplayer(level_.root())["mp_scenario"].to_string(); +} + bool game::allow_observers() const { return get_multiplayer(level_.root())["observer"].to_bool(true); diff --git a/src/server/wesnothd/game.hpp b/src/server/wesnothd/game.hpp index 925874b7fdd..55b3035d708 100644 --- a/src/server/wesnothd/game.hpp +++ b/src/server/wesnothd/game.hpp @@ -37,6 +37,7 @@ class game public: game(wesnothd::server& server, player_connections& player_connections, player_iterator host, + bool is_queue_game, const std::string& name = "", bool save_replays = false, const std::string& replay_save_path = ""); @@ -139,6 +140,8 @@ public: return level_.child("snapshot") || level_.child("scenario"); } + const std::string get_scenario_id() const; + /** * The non-const version. * @@ -611,6 +614,15 @@ public: observers_.clear(); } + bool is_queue_game() const + { + return is_queue_game_; + } + void is_queue_game(bool is_queue_game) + { + is_queue_game_ = is_queue_game; + } + private: // forbidden operations game(const game&) = delete; @@ -956,6 +968,9 @@ private: * New requests should never have a lower value than this. */ int last_choice_request_id_; + + /** Whether this game was created by joining a game defined client-side in an [mp_queue] */ + bool is_queue_game_; }; } // namespace wesnothd diff --git a/src/server/wesnothd/server.cpp b/src/server/wesnothd/server.cpp index ac196551be6..7caf0ca88d3 100644 --- a/src/server/wesnothd/server.cpp +++ b/src/server/wesnothd/server.cpp @@ -1376,15 +1376,16 @@ void server::handle_create_game(player_iterator player, simple_wml::node& create const std::string game_name = create_game["name"].to_string(); const std::string game_password = create_game["password"].to_string(); const std::string initial_bans = create_game["ignored"].to_string(); + const bool is_queue_game = create_game["queue_game"].to_bool(); DBG_SERVER << player->client_ip() << "\t" << player->info().name() << "\tcreates a new game: \"" << game_name << "\"."; // Create the new game, remove the player from the lobby // and set the player as the host/owner. - player_connections_.modify(player, [this, player, &game_name](player_record& host_record) { + player_connections_.modify(player, [this, player, &game_name, is_queue_game](player_record& host_record) { host_record.get_game().reset( - new wesnothd::game(*this, player_connections_, player, game_name, save_replays_, replay_save_path_), + new wesnothd::game(*this, player_connections_, player, is_queue_game, game_name, save_replays_, replay_save_path_), std::bind(&server::cleanup_game, this, std::placeholders::_1) ); }); @@ -1439,9 +1440,43 @@ void server::cleanup_game(game* game_ptr) void server::handle_join_game(player_iterator player, simple_wml::node& join) { + int game_id = join["id"].to_int(); + + // this is a game defined in an [mp_queue] in the client + // if there is no mp_queue defined game already existing with empty slots, tell the client to create one + // else update game_id to the game that already exists and have the client join that game + if(game_id < 0) { + for(const auto& game : games()) { + if(game->is_queue_game() && + !game->started() && + join["mp_scenario"].to_string() == game->get_scenario_id() && + game->description()->child("slot_data")->attr("vacant").to_int() != 0) { + game_id = game->id(); + } + } + + // if it's still negative, then there's no existing game to join + if(game_id < 0) { + simple_wml::document create_game_doc; + simple_wml::node& create_game_node = create_game_doc.root().add_child("create_game"); + create_game_node.set_attr_dup("mp_scenario", join["mp_scenario"].to_string().c_str()); + + // tell the client to create a game since there is no suitable existing game to join + send_to_player(player, create_game_doc); + return; + } else { + simple_wml::document join_game_doc; + simple_wml::node& join_game_node = join_game_doc.root().add_child("join_game"); + join_game_node.set_attr_int("id", game_id); + + // tell the client to create a game since there is no suitable existing game to join + send_to_player(player, join_game_doc); + return; + } + } + const bool observer = join.attr("observe").to_bool(); const std::string& password = join["password"].to_string(); - int game_id = join["id"].to_int(); auto g_iter = player_connections_.get().find(game_id);