wesnoth/src/multiplayer_connect.cpp

813 lines
24 KiB
C++

/* $Id$ */
/*
Copyright (C) 2003 by David White <davidnwhite@optusnet.com.au>
Part of the Battle for Wesnoth Project http://wesnoth.whitevine.net
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "events.hpp"
#include "font.hpp"
#include "language.hpp"
#include "log.hpp"
#include "image.hpp"
#include "mapgen.hpp"
#include "multiplayer.hpp"
#include "multiplayer_client.hpp"
#include "multiplayer_connect.hpp"
#include "network.hpp"
#include "playlevel.hpp"
#include "preferences.hpp"
#include "replay.hpp"
#include "show_dialog.hpp"
#include "widgets/textbox.hpp"
#include "widgets/button.hpp"
#include "widgets/combo.hpp"
#include "widgets/menu.hpp"
#include "widgets/slider.hpp"
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
mp_connect::mp_connect(display& disp, std::string game_name,
config &cfg, game_data& data, game_state& state,
bool join) :
disp_(&disp), cfg_(&cfg), data_(&data), state_(&state),
show_replay_(false), save_(false), status_(0), join_(join),
player_types_(), player_races_(), player_teams_(),
player_colors_(), combos_type_(), combos_race_(),
combos_team_(), combos_color_(), sliders_gold_(),
launch_(gui::button(disp, string_table["im_ready"])),
cancel_(gui::button(disp, string_table["cancel"])),
ai_(gui::button(disp, string_table["ai_players"])),
width_(630), height_(290), full_(false)
{
// Send Initial information
config response;
config& create_game = response.add_child("create_game");
create_game["name"] = game_name;
network::send_data(response);
}
mp_connect::~mp_connect()
{
if(network::nconnections() > 0) {
config cfg;
cfg.add_child("leave_game");
network::send_data(cfg);
}
}
int mp_connect::load_map(int map, int num_turns, int village_gold,
bool fog_game, bool shroud_game)
{
log_scope("load_map");
// Setup the game
config* level_ptr;
const config::child_list& levels = cfg_->get_children("multiplayer");
if(map == levels.size() )
{
//Load a saved game
save_ = true;
bool show_replay = false;
const std::string game = dialogs::load_game_dialog(*disp_, &show_replay);
if(game == "")
{
status_ = -1;
return status_;
}
log_scope("loading save");
load_game(*data_, game, *state_);
state_->gold = -10000;
state_->available_units.clear();
state_->can_recruit.clear();
if(state_->campaign_type != "multiplayer") {
gui::show_dialog(*disp_, NULL, "",
string_table["not_multiplayer_save_message"],
gui::OK_ONLY);
status_ = -1;
return status_;
}
if(state_->version != game_config::version) {
const int res = gui::show_dialog(*disp_, NULL, "",
string_table["version_save_message"],
gui::YES_NO);
if(res == 1) {
status_ = -1;
return status_;
}
}
loaded_level_ = state_->starting_pos;
level_ptr = &loaded_level_;
//make all sides untaken
for(config::child_itors i = level_ptr->child_range("side");
i.first != i.second; ++i.first) {
(**i.first)["taken"] = "";
//tell clients not to change their race
(**i.first)["allow_changes"] = "no";
}
recorder = replay(state_->replay_data);
config* const start = level_ptr->child("start");
//if this is a snapshot save, we don't want to use the replay data
if(loaded_level_["snapshot"] == "yes") {
if(start != NULL)
start->clear_children("replay");
level_ptr->clear_children("replay");
recorder.set_to_end();
} else {
//add the replay data under the level data so clients can
//receive it
level_ptr->clear_children("replay");
level_ptr->add_child("replay") = state_->replay_data;
}
} else {
//Load a new map
save_ = false;
level_ptr = levels[map];
//set the number of turns here
std::stringstream turns;
turns << num_turns;
(*level_ptr)["turns"] = turns.str();
}
assert(level_ptr != NULL);
level_ = level_ptr;
state_->label = level_->values["name"];
std::map<int,std::string> res_to_id;
for(config::child_list::const_iterator i = levels.begin(); i != levels.end(); ++i){
const std::string& id = (**i)["id"];
res_to_id[i - levels.begin()] = id;
}
state_->scenario = res_to_id[map];
const config::child_itors sides = level_->child_range("side");
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
if(sides.first == sides.second || possible_sides.empty()) {
gui::show_dialog(*disp_, NULL, "",
string_table["error_no_mp_sides"],
gui::OK_ONLY);
std::cerr << "no multiplayer sides found\n";
status_ = -1;
return status_;
}
config::child_iterator sd;
if(save_ == true) {
bool found = false;
for(sd = sides.first; sd != sides.second; ++sd) {
if ((**sd)["description"] == preferences::login())
found = true;
}
if (found == false) {
gui::show_dialog(*disp_, NULL, "",
string_table["error_not_part_of_game"],
gui::OK_ONLY);
status_ = -1;
return status_;
}
}
bool first = true;
for(sd = sides.first; sd != sides.second; ++sd) {
if(save_ == false)
{
std::stringstream svillage_gold;
svillage_gold << village_gold;
(**sd)["village_gold"] = svillage_gold.str();
(**sd)["gold"] = "100";
if (first == true) {
(**sd)["controller"] = "human";
(**sd)["description"] = preferences::login();
first = false;
} else {
(**sd)["controller"] = "network";
(**sd)["description"] = "";
}
}
if((**sd)["fog"].empty())
(**sd)["fog"] = fog_game ? "yes" : "no";
if((**sd)["shroud"].empty())
(**sd)["shroud"] = shroud_game ? "yes" : "no";
if((**sd)["name"].empty())
(**sd)["name"] = (*possible_sides.front())["name"];
if((**sd)["type"].empty())
(**sd)["type"] = (*possible_sides.front())["type"];
if((**sd)["recruit"].empty())
(**sd)["recruit"] = (*possible_sides.front())["recruit"];
if((**sd)["music"].empty())
(**sd)["music"] = (*possible_sides.front())["music"];
if((**sd)["recruitment_pattern"].empty())
(**sd)["recruitment_pattern"] =
possible_sides.front()->values["recruitment_pattern"];
}
if ((*level_)["objectives"] == "")
{
(*level_)["objectives"] = "Victory:\n@Defeat enemy leader(s)\n";
}
lists_init();
gui_init();
status_ = 0;
//if we have any connected players when we are created, send them the data
network::send_data(*level_);
return status_;
}
void mp_connect::lists_init()
{
//Options
player_types_.push_back(string_table["network_controlled"]);
player_types_.push_back(string_table["human_controlled"]);
player_types_.push_back(string_table["ai_controlled"]);
player_types_.push_back("-----");
player_types_.push_back(preferences::login());
//Races
const config::child_itors sides = level_->child_range("side");
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
for(std::vector<config*>::const_iterator race = possible_sides.begin();
race != possible_sides.end(); ++race) {
player_races_.push_back(translate_string((**race)["name"]));
}
//Teams
config::child_iterator sd;
height_ = 120;
for(sd = sides.first; sd != sides.second; ++sd) {
height_ = height_ + 30;
const int team_num = sd - sides.first;
const std::string& team_name = (**sd)["team_name"];
std::stringstream str;
str << string_table["team"] << " ";
if(team_name.empty() == false)
str << team_name;
else
str << (team_num+1);
player_teams_.push_back(str.str());
}
//Colors
player_colors_.push_back(string_table["red"]);
player_colors_.push_back(string_table["blue"]);
player_colors_.push_back(string_table["green"]);
player_colors_.push_back(string_table["yellow"]);
player_colors_.push_back(string_table["pink"]);
player_colors_.push_back(string_table["purple"]);
}
void mp_connect::add_player(const std::string& name)
{
player_types_.push_back(name);
for(size_t n = 0; n != combos_type_.size(); ++n) {
combos_type_[n].set_items(player_types_);
}
}
void mp_connect::remove_player(const std::string& name)
{
const std::vector<std::string>::iterator itor = std::find(player_types_.begin(),player_types_.end(),name);
if(itor != player_types_.end())
player_types_.erase(itor);
}
void mp_connect::gui_init()
{
// Wait to players, Configure players
gui::draw_dialog_frame((disp_->x()-width_)/2, (disp_->y()-height_)/2,
width_, height_, *disp_);
//Buttons
launch_.set_xy((disp_->x()/2)-launch_.width()/2-100,
(disp_->y()-height_)/2+height_-30);
cancel_.set_xy((disp_->x()/2)-launch_.width()/2+100,
(disp_->y()-height_)/2+height_-30);
ai_.set_xy((disp_->x()-width_)/2+30,
(disp_->y()-height_)/2+height_-60);
//Title and labels
SDL_Rect labelr;
font::draw_text(disp_,disp_->screen_area(),24,font::NORMAL_COLOUR,
string_table["game_lobby"],-1,(disp_->y()-height_)/2+5);
labelr.x=0; labelr.y=0; labelr.w=disp_->x(); labelr.h=disp_->y();
labelr = font::draw_text(NULL,labelr,14,font::GOOD_COLOUR,
string_table["player_type"],0,0);
font::draw_text(disp_,disp_->screen_area(),14,font::GOOD_COLOUR,
string_table["player_type"],((disp_->x()-width_)/2+30)+(launch_.width()/2)-(labelr.w/2),
(disp_->y()-height_)/2+35);
labelr.x=0; labelr.y=0; labelr.w=disp_->x(); labelr.h=disp_->y();
labelr = font::draw_text(NULL,labelr,14,font::GOOD_COLOUR,
string_table["race"],0,0);
font::draw_text(disp_,disp_->screen_area(),14,font::GOOD_COLOUR,
string_table["race"],((disp_->x()-width_)/2+145)+(launch_.width()/2)-(labelr.w/2),
(disp_->y()-height_)/2+35);
labelr.x=0; labelr.y=0; labelr.w=disp_->x(); labelr.h=disp_->y();
labelr = font::draw_text(NULL,labelr,14,font::GOOD_COLOUR,
string_table["team"],0,0);
font::draw_text(disp_,disp_->screen_area(),14,font::GOOD_COLOUR,
string_table["team"],((disp_->x()-width_)/2+260)+(launch_.width()/2)-(labelr.w/2),
(disp_->y()-height_)/2+35);
labelr.x=0; labelr.y=0; labelr.w=disp_->x(); labelr.h=disp_->y();
labelr = font::draw_text(NULL,labelr,14,font::GOOD_COLOUR,
string_table["color"],0,0);
font::draw_text(disp_,disp_->screen_area(),14,font::GOOD_COLOUR,
string_table["color"],((disp_->x()-width_)/2+375)+(launch_.width()/2)-(labelr.w/2),
(disp_->y()-height_)/2+35);
labelr.x=0; labelr.y=0; labelr.w=disp_->x(); labelr.h=disp_->y();
labelr = font::draw_text(NULL,labelr,14,font::GOOD_COLOUR,
string_table["gold"],0,0);
font::draw_text(disp_,disp_->screen_area(),14,font::GOOD_COLOUR,
string_table["gold"],((disp_->x()-width_)/2+480)+(launch_.width()/2)-(labelr.w/2),
(disp_->y()-height_)/2+35);
//Per player settings
const config::child_itors sides = level_->child_range("side");
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
config::child_iterator sd;
SDL_Rect rect;
for(sd = sides.first; sd != sides.second; ++sd) {
const int side_num = sd - sides.first;
//Player number
font::draw_text(disp_,disp_->screen_area(), 24, font::GOOD_COLOUR,
(*sd)->values["side"], (disp_->x()-width_)/2+10,
(disp_->y()-height_)/2+53+(30*side_num));
//Player type
combos_type_.push_back(gui::combo(*disp_, player_types_));
combos_type_.back().set_xy((disp_->x()-width_)/2+30,
(disp_->y()-height_)/2+55+(30*side_num));
//Player race
combos_race_.push_back(gui::combo(*disp_, player_races_));
combos_race_.back().set_xy((disp_->x()-width_)/2+145,
(disp_->y()-height_)/2+55+(30*side_num));
//Player team
combos_team_.push_back(gui::combo(*disp_, player_teams_));
combos_team_.back().set_xy((disp_->x()-width_)/2+260,
(disp_->y()-height_)/2+55+(30*side_num));
combos_team_.back().set_selected(side_num);
//Player color
combos_color_.push_back(gui::combo(*disp_, player_colors_));
combos_color_.back().set_xy((disp_->x()-width_)/2+375,
(disp_->y()-height_)/2+55+(30*side_num));
combos_color_.back().set_selected(side_num);
//Player gold
rect.x = (disp_->x()-width_)/2+490;
rect.y = (disp_->y()-height_)/2+55+(30*side_num);
rect.w = launch_.width()-5;
rect.h = launch_.height();
sliders_gold_.push_back(gui::slider(*disp_, rect, 0.0+((80.0)/979.0)));
rect.w = 30;
rect.x = (disp_->x()-width_)/2+603;
gold_bg_.push_back(surface_restorer(&disp_->video(),rect));
font::draw_text(disp_, disp_->screen_area(), 12, font::GOOD_COLOUR,
"100", rect.x, rect.y);
}
update_whole_screen();
}
void mp_connect::gui_update()
{
//Update the GUI based on current config settings.
//Settings may change based on other networked
//players.
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
const config::child_itors sides = level_->child_range("side");
SDL_Rect rect;
for(size_t n = 0; n != combos_type_.size(); ++n) {
config& side = **(sides.first+n);
//Player type
if (side["controller"] == "network") {
if (side["description"] == "") {
combos_type_[n].set_selected(0);
} else if (side["description"] == "Computer Player") {
//When loading a game you did not create AI players are marked
//as network players, set back to AI
combos_type_[n].set_selected(2);
} else {
for (size_t m = 0; m != player_types_.size(); ++m) {
if (side["description"] == player_types_[m]) {
combos_type_[n].set_selected(m);
}
}
}
} else if (side["controller"] == "human") {
if (side["description"] == preferences::login()) {
combos_type_[n].set_selected(4);
} else if (side["description"] != "") {
//When loading a game and you use a name not originally used during
//the initial game, mark that original slot as network
combos_type_[n].set_selected(0);
} else {
combos_type_[n].set_selected(1);
}
} else if (side["controller"] == "ai") {
combos_type_[n].set_selected(2);
}
//Player Race
for (size_t m = 0; m != player_races_.size(); ++m) {
if (translate_string(side["name"]) == player_races_[m]) {
combos_race_[n].set_selected(m);
}
}
//Player Team
const std::string& team_name = side["team_name"];
if(team_name != "" && isdigit(team_name[0]))
combos_team_[n].set_selected(team_name[0] - '1');
//Player Color
//Player Gold
std::string str = side["gold"];
sliders_gold_[n].set_value((atoi(str.c_str()) - 20 + 0.0) / 979.0);
rect.x = (disp_->x() - width_) / 2 + 603;
rect.y = (disp_->y() - height_) / 2 + 55 + (30 * n);
rect.w = 30;
rect.h = launch_.height();
gold_bg_[n].restore();
font::draw_text(disp_, disp_->screen_area(), 12,
font::GOOD_COLOUR,
side["gold"],
rect.x, rect.y);
update_rect(rect);
}
}
int mp_connect::gui_do()
{
SDL_Rect rect;
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
const config::child_itors sides = level_->child_range("side");
int new_playergold = -1;
int cur_playergold = -1;
for(;;) {
int mousex, mousey;
const int mouse_flags = SDL_GetMouseState(&mousex,&mousey);
const bool left_button = mouse_flags&SDL_BUTTON_LMASK;
for(size_t n = 0; n != combos_team_.size(); ++n) {
config& side = **(sides.first+n);
//Player type
//Don't let user change this if a player is sitting
combos_type_[n].enable(combos_type_[n].selected() < 4);
int old_select = combos_type_[n].selected();
if(combos_type_[n].process(mousex, mousey, left_button)) {
if(combos_type_[n].selected() == 0) {
side["controller"] = "network";
side["description"] = "";
}else if(combos_type_[n].selected() == 1){
side["controller"] = "human";
side["description"] = "";
}else if(combos_type_[n].selected() == 2){
side["controller"] = "ai";
side["description"] = string_table["ai_controlled"];
}else if(combos_type_[n].selected() == 3){
combos_type_[n].set_selected(old_select);
}else if(combos_type_[n].selected() == 4){
side["controller"] = "human";
side["description"] = preferences::login();
for(size_t m = 0; m != combos_type_.size(); ++m) {
if(m != n) {
if(combos_type_[m].selected() == 4){
combos_type_[m].set_selected(0);
config& si = **(sides.first+m);
si["controller"] = "network";
si["description"] = "";
}
}
}
}else{
side["controller"] = "network";
side["description"] = "";
}
network::send_data(*level_);
}
//Player race
combos_race_[n].enable(!save_);
combos_team_[n].enable(!save_);
combos_color_[n].enable(!save_);
if(combos_race_[n].process(mousex, mousey, left_button)) {
const string_map& values = possible_sides[combos_race_[n].selected()]->values;
for(string_map::const_iterator i = values.begin(); i != values.end(); ++i) {
side[i->first] = i->second;
}
network::send_data(*level_);
}
//Player team
if(combos_team_[n].process(mousex, mousey, left_button)) {
std::stringstream str;
str << (combos_team_[n].selected()+1);
side["team_name"] = str.str();
network::send_data(*level_);
}
if(combos_color_[n].process(mousex, mousey, left_button)) {
network::send_data(*level_);
}
if(!save_){
int check_playergold = 20 + int(979 *
sliders_gold_[n].process(mousex, mousey, left_button));
if(abs(check_playergold) == check_playergold)
new_playergold = check_playergold;
if(new_playergold != cur_playergold) {
cur_playergold = new_playergold;
std::stringstream playergold;
playergold << cur_playergold;
side["gold"] = playergold.str();
rect.x = (disp_->x() - width_) / 2 + 603;
rect.y = (disp_->y() - height_) / 2 + 55 + (30 * n);
rect.w = 30;
rect.h = launch_.height();
gold_bg_[n].restore();
font::draw_text(disp_, disp_->screen_area(), 12,
font::GOOD_COLOUR,
(*sides.first[n])["gold"],
rect.x, rect.y);
update_rect(rect);
network::send_data(*level_);
}
}else{
sliders_gold_[n].draw();
}
}
if(cancel_.process(mousex,mousey,left_button)) {
if(network::nconnections() > 0) {
config cfg;
cfg.add_child("leave_game");
network::send_data(cfg);
}
status_ = 0;
return status_;
}
if(ai_.process(mousex,mousey,left_button)) {
for(size_t m = 0; m != combos_team_.size(); ++m) {
config& si = **(sides.first+m);
si["controller"] = "ai";
si["description"] = string_table["ai_controlled"];
combos_type_[m].set_selected(2);
}
network::send_data(*level_);
}
launch_.enable(full_);
if(launch_.process(mousex,mousey,left_button)) {
//Tell everyone to start
config cfg;
cfg.add_child("start_game");
network::send_data(cfg);
state_->starting_pos = *level_;
recorder.set_save_info(*state_);
//see if we should show the replay of the game so far
if(!recorder.empty()) {
if(false) {
recorder.set_skip(0);
} else {
std::cerr << "skipping...\n";
recorder.set_skip(-1);
}
}
//any replay data isn't meant to hang around under the level,
//it was just there to tell clients about the replay data
level_->clear_children("replay");
std::vector<config*> story;
play_level(*data_, *cfg_, level_, disp_->video(), *state_, story);
recorder.clear();
if(network::nconnections() > 0) {
config cfg;
cfg.add_child("leave_game");
network::send_data(cfg);
}
status_ = 0;
return status_;
}
gui_update();
update_positions();
update_network();
is_full();
events::pump();
disp_->video().flip();
SDL_Delay(20);
}
return status_;
}
void mp_connect::update_positions()
{
const config::child_itors sides = level_->child_range("side");
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
config::child_iterator sd;
for(sd = sides.first; sd != sides.second; ++sd) {
if((**sd)["taken"] != "yes") {
positions_[*sd] = 0;
}
}
}
void mp_connect::update_network()
{
for(std::map<config*,network::connection>::const_iterator i = positions_.begin();
i != positions_.end(); ++i) {
if(!i->second) {
//We are waiting on someone
network::connection sock = network::accept_connection();
if(sock) {
std::cerr << "Received connection\n";
network::send_data(*level_,sock);
}
}
}
config cfg;
const config::child_list& sides = level_->get_children("side");
network::connection sock;
try {
sock = network::receive_data(cfg);
} catch(network::error& e) {
std::cerr << "caught networking error. we are " << (network::is_server() ? "" : "NOT") << " a server\n";
sock = 0;
//if the problem isn't related to any specific connection,
//it's a general error and we should just re-throw the error
//likewise if we are not a server, we cannot afford any connection
//to go down, so also re-throw the error
if(!e.socket || !network::is_server()) {
e.disconnect();
throw network::error(e.message);
}
bool changes = false;
//a socket has disconnected. Remove its positions.
for(std::map<config*,network::connection>::iterator i = positions_.begin();
i != positions_.end(); ++i) {
if(i->second == e.socket) {
changes = true;
i->second = 0;
i->first->values.erase("taken");
remove_player(i->first->values["description"]);
i->first->values["description"] = "";
}
}
//now disconnect the socket
e.disconnect();
//if there have been changes to the positions taken,
//then notify other players
if(changes) {
network::send_data(*level_);
}
}
//No network errors
if(sock) {
const int side_drop = atoi(cfg["side_drop"].c_str())-1;
if(side_drop >= 0 && side_drop < int(sides.size())) {
std::map<config*,network::connection>::iterator pos = positions_.find(sides[side_drop]);
if(pos != positions_.end()) {
pos->second = 0;
pos->first->values.erase("taken");
pos->first->values["description"] = "";
network::send_data(*level_);
}
}
const int side_taken = atoi(cfg["side"].c_str())-1;
if(side_taken >= 0 && side_taken < int(sides.size())) {
std::map<config*,network::connection>::iterator pos = positions_.find(sides[side_taken]);
if(pos != positions_.end()) {
if(!pos->second || pos->second == sock) {
std::cerr << "client has taken a valid position\n";
//does the client already own the side, and is just updating
//it, or is it taking a vacant slot?
const bool update_only = pos->second == sock;
//broadcast to everyone the new game status
pos->first->values["controller"] = "network";
pos->first->values["taken"] = "yes";
pos->first->values["description"] = cfg["description"];
pos->first->values["name"] = cfg["name"];
pos->first->values["type"] = cfg["type"];
pos->first->values["recruit"] = cfg["recruit"];
pos->first->values["music"] = cfg["music"];
pos->second = sock;
network::send_data(*level_);
std::cerr << "sent player data\n";
//send a reply telling the client they have secured
//the side they asked for
std::stringstream side;
side << (side_taken+1);
config reply;
reply.values["side_secured"] = side.str();
std::cerr << "going to send data...\n";
network::send_data(reply,sock);
// Add to combo list
add_player(cfg["description"]);
} else {
config response;
response.values["failed"] = "yes";
network::send_data(response,sock);
}
} else {
std::cerr << "tried to take illegal side: " << side_taken << "\n";
}
} else {
std::cerr << "tried to take unknown side: " << side_taken << "\n";
}
}
}
void mp_connect::is_full()
{
//see if all positions are now filled
full_ = true;
const config::child_itors sides = level_->child_range("side");
const config::child_list& possible_sides = cfg_->get_children("multiplayer_side");
config::child_iterator sd;
for(sd = sides.first; sd != sides.second; ++sd) {
if((**sd)["controller"] == "network" &&
(**sd)["description"] == "") {
if(positions_[*sd] == 0) {
full_ = false;
}
}
}
}