wesnoth/src/playcampaign.cpp
2013-03-26 21:41:37 -04:00

749 lines
24 KiB
C++

/*
Copyright (C) 2003-2005 by David White <dave@whitevine.net>
Copyright (C) 2005 - 2013 by Philippe Plantier <ayin@anathas.org>
Part of the Battle for Wesnoth Project http://www.wesnoth.org
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.
See the COPYING file for more details.
*/
/**
* @file
* Controls setup, play, (auto)save and replay of campaigns.
*/
#include "global.hpp"
#include "game_preferences.hpp"
#include "gui/dialogs/message.hpp"
#include "gui/dialogs/transient_message.hpp"
#include "gui/widgets/window.hpp"
#include "playcampaign.hpp"
#include "map_create.hpp"
#include "persist_manager.hpp"
#include "playmp_controller.hpp"
#include "replay_controller.hpp"
#include "log.hpp"
#include "map_exception.hpp"
#include "dialogs.hpp"
#include "gettext.hpp"
#include "resources.hpp"
#include "savegame.hpp"
#include "sound.hpp"
#include "wml_exception.hpp"
#include "formula_string_utils.hpp"
#include <boost/foreach.hpp>
#define LOG_G LOG_STREAM(info, lg::general)
static lg::log_domain log_engine("engine");
#define LOG_NG LOG_STREAM(info, log_engine)
static lg::log_domain log_enginerefac("enginerefac");
#define LOG_RG LOG_STREAM(info, log_enginerefac)
namespace {
struct player_controller
{
player_controller() :
controller(),
description()
{}
player_controller(const std::string& controller, const std::string& description) :
controller(controller),
description(description)
{}
std::string controller;
std::string description;
};
typedef std::map<std::string, player_controller> controller_map;
} // end anon namespace
static void team_init(config& level, game_state& gamestate){
//if we are at the start of a new scenario, initialize carryover_sides
if(gamestate.snapshot.child_or_empty("variables")["turn_number"].to_int(-1)<1){
gamestate.carryover_sides = gamestate.carryover_sides_start;
}
carryover_info sides(gamestate.carryover_sides);
sides.transfer_to(level);
BOOST_FOREACH(config& side_cfg, level.child_range("side")){
sides.transfer_all_to(side_cfg);
}
gamestate.carryover_sides = sides.to_config();
}
static void mp_events_init(config& level, game_state& gamestate, const config& gamecfg){
const std::string& era = gamestate.mp_settings().mp_era;
if (!era.empty()) {
level.add_child("era", gamecfg.find_child("era", "id", era));
}
const std::vector<std::string>& mods = gamestate.mp_settings().active_mods;
BOOST_FOREACH (const std::string& mod, mods) {
level.add_child("modification", gamecfg.find_child("modification", "id", mod));
}
}
static void store_carryover(game_state& gamestate, playsingle_controller& playcontroller, display& disp, const end_level_data& end_level){
bool has_next_scenario = !resources::gamedata->next_scenario().empty() &&
resources::gamedata->next_scenario() != "null";
if(resources::teams->size() < 1){
gamestate.carryover_sides_start["next_scenario"] = resources::gamedata->next_scenario();
return;
}
carryover_info sides(gamestate.carryover_sides);
sides.transfer_from(*resources::gamedata);
std::ostringstream report;
std::string title;
bool obs = is_observer();
if (obs) {
title = _("Scenario Report");
} else {
title = _("Victory");
report << "<b>" << _("You have emerged victorious!") << "</b>\n\n";
}
std::vector<team> teams = playcontroller.get_teams_const();
int persistent_teams = 0;
BOOST_FOREACH(const team &t, teams) {
if (t.persistent()){
++persistent_teams;
}
}
if (persistent_teams > 0 && (has_next_scenario ||
gamestate.classification().campaign_type == "test"))
{
gamemap map = playcontroller.get_map_const();
int finishing_bonus_per_turn =
map.villages().size() * game_config::village_income +
game_config::base_income;
tod_manager tod = playcontroller.get_tod_manager_const();
int turns_left = std::max<int>(0, tod.number_of_turns() - tod.turn());
int finishing_bonus = (end_level.gold_bonus && turns_left > -1) ?
finishing_bonus_per_turn * turns_left : 0;
BOOST_FOREACH(const team &t, teams)
{
if (!t.persistent()){
continue;
}
int carryover_gold = div100rounded((t.gold() + finishing_bonus) * end_level.carryover_percentage);
sides.transfer_from(t, carryover_gold);
if (!t.is_human()){
continue;
}
if (persistent_teams > 1) {
report << "\n<b>" << t.current_player() << "</b>\n";
}
playcontroller.report_victory(report, carryover_gold, t.gold(), finishing_bonus_per_turn, turns_left, finishing_bonus);
}
}
if (end_level.transient.carryover_report) {
gui2::show_transient_message(disp.video(), title, report.str(), "", true);
}
gamestate.carryover_sides_start = sides.to_config();
}
void play_replay(display& disp, game_state& gamestate, const config& game_config,
CVideo& video)
{
std::string type = gamestate.classification().campaign_type;
if(type.empty())
type = "scenario";
// 'starting_pos' will contain the position we start the game from.
config starting_pos;
if (gamestate.replay_start().empty()){
// Backwards compatibility code for 1.2 and 1.2.1
const config &scenario = game_config.find_child(type,"id",gamestate.carryover_sides_start["next_scenario"]);
assert(scenario);
gamestate.replay_start() = scenario;
}
starting_pos = gamestate.replay_start();
//for replays, use the variables specified in starting_pos
if (const config &vars = starting_pos.child("variables")) {
gamestate.carryover_sides_start.child_or_add("variables") = vars;
}
try {
// Preserve old label eg. replay
if (gamestate.classification().label.empty())
gamestate.classification().label = starting_pos["name"].str();
//if (gamestate.abbrev.empty())
// gamestate.abbrev = (*scenario)["abbrev"];
play_replay_level(game_config, &starting_pos, video, gamestate);
gamestate.snapshot = config();
recorder.clear();
gamestate.replay_data.clear();
} catch(game::load_game_failed& e) {
gui2::show_error_message(disp.video(), _("The game could not be loaded: ") + e.message);
} catch(game::game_error& e) {
gui2::show_error_message(disp.video(), _("Error while playing the game: ") + e.message);
} catch(incorrect_map_format_error& e) {
gui2::show_error_message(disp.video(), std::string(_("The game map could not be loaded: ")) + e.message);
} catch(twml_exception& e) {
e.show(disp);
}
}
static LEVEL_RESULT playsingle_scenario(const config& game_config,
const config* level, display& disp, game_state& state_of_game,
const config::const_child_itors &story,
bool skip_replay, end_level_data &end_level)
{
const int ticks = SDL_GetTicks();
int num_turns = (*level)["turns"].to_int(-1);
config init_level = *level;
team_init(init_level, state_of_game);
LOG_NG << "creating objects... " << (SDL_GetTicks() - ticks) << "\n";
playsingle_controller playcontroller(init_level, state_of_game, ticks, num_turns, game_config, disp.video(), skip_replay);
LOG_NG << "created objects... " << (SDL_GetTicks() - playcontroller.get_ticks()) << "\n";
LEVEL_RESULT res = playcontroller.play_scenario(story, skip_replay);
end_level = playcontroller.get_end_level_data_const();
config& cfg_end_level = state_of_game.carryover_sides.child("end_level_data");
end_level.write(cfg_end_level);
if (res == DEFEAT) {
if (resources::persist != NULL)
resources::persist->end_transaction();
gui2::show_transient_message(disp.video(),
_("Defeat"),
_("You have been defeated!")
);
}
else if(res == VICTORY){
store_carryover(state_of_game, playcontroller, disp, end_level);
}
if (!disp.video().faked() && res != QUIT && end_level.transient.linger_mode)
{
try {
playcontroller.linger();
} catch(end_level_exception& e) {
if (e.result == QUIT) {
return QUIT;
}
}
}
return res;
}
static LEVEL_RESULT playmp_scenario(const config& game_config,
const config* level, display& disp, game_state& state_of_game,
const config::const_child_itors &story, bool skip_replay,
io_type_t& io_type, end_level_data &end_level)
{
const int ticks = SDL_GetTicks();
int num_turns = (*level)["turns"].to_int(-1);
config init_level = *level;
team_init(init_level, state_of_game);
mp_events_init(init_level, state_of_game, game_config);
playmp_controller playcontroller(init_level, state_of_game, ticks, num_turns,
game_config, disp.video(), skip_replay, io_type == IO_SERVER);
LEVEL_RESULT res = playcontroller.play_scenario(story, skip_replay);
end_level = playcontroller.get_end_level_data_const();
config& cfg_end_level = state_of_game.carryover_sides.child("end_level_data");
end_level.write(cfg_end_level);
//Check if the player started as mp client and changed to host
if (io_type == IO_CLIENT && playcontroller.is_host())
io_type = IO_SERVER;
if (res == DEFEAT) {
if (resources::persist != NULL)
resources::persist->end_transaction();
gui2::show_transient_message(disp.video(),
_("Defeat"),
_("You have been defeated!")
);
}
else if(res == VICTORY){
store_carryover(state_of_game, playcontroller, disp, end_level);
}
else if(res == OBSERVER_END){
state_of_game.carryover_sides_start["next_scenario"] = resources::gamedata->next_scenario();
}
if (!disp.video().faked() && res != QUIT) {
if (!end_level.transient.linger_mode) {
if(!playcontroller.is_host()) {
// If we continue without lingering we need to
// make sure the host uploads the next scenario
// before we attempt to download it.
playcontroller.wait_for_upload();
}
} else {
try {
playcontroller.linger();
} catch(end_level_exception& e) {
if (e.result == QUIT) {
return QUIT;
}
}
}
}
return res;
}
LEVEL_RESULT play_game(display& disp, game_state& gamestate, const config& game_config,
io_type_t io_type, bool skip_replay)
{
std::string type = gamestate.classification().campaign_type;
if(type.empty())
type = "scenario";
config const* scenario = NULL;
// 'starting_pos' will contain the position we start the game from.
config starting_pos;
carryover_info sides = carryover_info(gamestate.carryover_sides_start);
// Do we have any snapshot data?
// yes => this must be a savegame
// no => we are starting a fresh scenario
if (!gamestate.snapshot.child("side") || !recorder.at_end())
{
gamestate.classification().completion = "running";
// Campaign or Multiplayer?
// If the gamestate already contains a starting_pos,
// then we are starting a fresh multiplayer game.
// Otherwise this is the start of a campaign scenario.
if(gamestate.replay_start()["id"].empty() == false) {
starting_pos = gamestate.replay_start();
scenario = &starting_pos;
if(gamestate.replay_start()["random_seed"] != gamestate.carryover_sides_start["random_seed"]){
sides = carryover_info(gamestate.replay_start());
}
} else {
//reload of the scenario, as starting_pos contains carryover information only
LOG_G << "loading scenario: '" << sides.next_scenario() << "'\n";
scenario = &game_config.find_child(type, "id", sides.next_scenario());
if(!*scenario){
scenario = NULL;
}
LOG_G << "scenario found: " << (scenario != NULL ? "yes" : "no") << "\n";
}
} else {
// This game was started from a savegame
LOG_G << "loading snapshot...\n";
starting_pos = gamestate.replay_start();
scenario = &gamestate.snapshot;
// When starting wesnoth --multiplayer there might be
// no variables which leads to a segfault
if (const config &vars = gamestate.snapshot.child("variables")) {
sides.set_variables(vars);
}
sides.get_wml_menu_items().set_menu_items(gamestate.snapshot);
// Replace game label with that from snapshot
if (!gamestate.snapshot["label"].empty()){
gamestate.classification().label = gamestate.snapshot["label"].str();
}
}
gamestate.carryover_sides_start = sides.to_config();
controller_map controllers;
if(io_type == IO_SERVER) {
BOOST_FOREACH(config &side, const_cast<config *>(scenario)->child_range("side"))
{
if (side["current_player"] == preferences::login()) {
side["controller"] = preferences::client_type();
}
std::string id = side["save_id"];
if(id.empty())
continue;
controllers[id] = player_controller(side["controller"], side["id"]);
}
}
while(scenario != NULL) {
// If we are a multiplayer client, tweak the controllers
if(io_type == IO_CLIENT) {
if(scenario != &starting_pos) {
starting_pos = *scenario;
scenario = &starting_pos;
}
BOOST_FOREACH(config &side, starting_pos.child_range("side"))
{
if (side["current_player"] == preferences::login()) {
side["controller"] = preferences::client_type();
} else if (side["controller"] != "null") {
/// @todo Fix logic to use network_ai controller
// if it is networked ai
side["controller"] = "network";
}
}
}
config::const_child_itors story = scenario->child_range("story");
//TODO: remove once scenario in carryover_info/gamedata is confirmed
// gamestate.classification().next_scenario = (*scenario)["next_scenario"].str();
bool save_game_after_scenario = true;
LEVEL_RESULT res = VICTORY;
end_level_data end_level;
try {
// Preserve old label eg. replay
if (gamestate.classification().label.empty()) {
if (gamestate.classification().abbrev.empty())
gamestate.classification().label = (*scenario)["name"].str();
else {
gamestate.classification().label = gamestate.classification().abbrev;
gamestate.classification().label.append("-");
gamestate.classification().label.append((*scenario)["name"]);
}
}
// If the entire scenario should be randomly generated
if((*scenario)["scenario_generation"] != "") {
LOG_G << "randomly generating scenario...\n";
const cursor::setter cursor_setter(cursor::WAIT);
static config scenario2;
scenario2 = random_generate_scenario((*scenario)["scenario_generation"], scenario->child("generator"));
//TODO comment or remove
//level_ = scenario;
//merge carryover information into the newly generated scenario
scenario = &scenario2;
}
std::string map_data = (*scenario)["map_data"];
if(map_data.empty() && (*scenario)["map"] != "") {
map_data = read_map((*scenario)["map"]);
}
// If the map should be randomly generated
if(map_data.empty() && (*scenario)["map_generation"] != "") {
const cursor::setter cursor_setter(cursor::WAIT);
map_data = random_generate_map((*scenario)["map_generation"],scenario->child("generator"));
// Since we've had to generate the map,
// make sure that when we save the game,
// it will not ask for the map to be generated again on reload
static config new_level;
new_level = *scenario;
new_level["map_data"] = map_data;
scenario = &new_level;
LOG_G << "generated map\n";
}
sound::empty_playlist();
//add the variables to the starting_pos unless they are already there
const config &wmlvars = gamestate.replay_start().child("variables");
if (!wmlvars || wmlvars.empty()){
gamestate.replay_start().clear_children("variables");
gamestate.replay_start().add_child("variables", gamestate.carryover_sides_start.child_or_empty("variables"));
}
switch (io_type){
case IO_NONE:
res = playsingle_scenario(game_config, scenario, disp, gamestate, story, skip_replay, end_level);
break;
case IO_SERVER:
case IO_CLIENT:
res = playmp_scenario(game_config, scenario, disp, gamestate, story, skip_replay, io_type, end_level);
break;
}
} catch(game::load_game_failed& e) {
gui2::show_error_message(disp.video(), _("The game could not be loaded: ") + e.message);
return QUIT;
} catch(game::game_error& e) {
gui2::show_error_message(disp.video(), _("Error while playing the game: ") + e.message);
return QUIT;
} catch(incorrect_map_format_error& e) {
gui2::show_error_message(disp.video(), std::string(_("The game map could not be loaded: ")) + e.message);
return QUIT;
} catch(config::error& e) {
std::cerr << "caught config::error...\n";
gui2::show_error_message(disp.video(), _("Error while reading the WML: ") + e.message);
return QUIT;
} catch(twml_exception& e) {
e.show(disp);
return QUIT;
}
// Save-management options fire on game end.
// This means: (a) we have a victory, or
// or (b) we're multiplayer live, in which
// case defeat is also game end. Someday,
// if MP campaigns ever work again, we might
// need to change this test.
if (res == VICTORY || (io_type != IO_NONE && res == DEFEAT)) {
if (preferences::delete_saves())
savegame::clean_saves(gamestate.classification().label);
if (preferences::save_replays() && end_level.replay_save) {
savegame::replay_savegame save(gamestate, preferences::compress_saves());
save.save_game_automatic(disp.video(), true);
}
}
recorder.clear();
gamestate.replay_data.clear();
gamestate.replay_start().clear();
// On DEFEAT, QUIT, or OBSERVER_END, we're done now
if (res != VICTORY)
{
if (res != OBSERVER_END || gamestate.carryover_sides_start["next_scenario"].empty()) {
gamestate.snapshot = config();
return res;
}
const int dlg_res = gui2::show_message(disp.video(), _("Game Over"),
_("This scenario has ended. Do you want to continue the campaign?"),
gui2::tmessage::yes_no_buttons);
if(dlg_res == gui2::twindow::CANCEL) {
gamestate.snapshot = config();
return res;
}
}
// Continue without saving is like a victory,
// but the save game dialog isn't displayed
if (!end_level.prescenario_save)
save_game_after_scenario = false;
//TODO: remove once scenario in carryover_info/gamedata is confirmed
// Switch to the next scenario.
//gamestate.classification().scenario = gamestate.classification().next_scenario;
sides = carryover_info(gamestate.carryover_sides_start);
sides.rng().rotate_random();
gamestate.carryover_sides_start = sides.to_config();
if(io_type == IO_CLIENT) {
if (gamestate.carryover_sides_start["next_scenario"].empty()) {
gamestate.snapshot = config();
return res;
}
// Ask for the next scenario data.
network::send_data(config("load_next_scenario"), 0);
config cfg;
std::string msg = _("Downloading next scenario...");
do {
cfg.clear();
network::connection data_res = dialogs::network_receive_dialog(disp,
msg, cfg);
if(!data_res) {
gamestate.snapshot = config();
return QUIT;
}
} while (!cfg.child("next_scenario"));
if (const config &c = cfg.child("next_scenario")) {
starting_pos = c;
scenario = &starting_pos;
gamestate = game_state(starting_pos);
//retain carryover_sides_start, as the config from the server doesn't contain it
gamestate.carryover_sides_start = sides.to_config();
} else {
gamestate.snapshot = config();
return QUIT;
}
} else {
scenario = &game_config.find_child(type, "id", gamestate.carryover_sides_start["next_scenario"]);
if (!*scenario)
scenario = NULL;
else
{
starting_pos = *scenario;
scenario = &starting_pos;
}
if(io_type == IO_SERVER && scenario != NULL) {
// Tweaks sides to adapt controllers and descriptions.
BOOST_FOREACH(config &side, starting_pos.child_range("side"))
{
std::string id = side["save_id"];
if(id.empty()) {
id = side["id"].str();
}
if(!id.empty()) {
/* Update side info to match current_player info
* to allow it taking the side in next scenario
* and to be set in the players list on side server
*/
controller_map::const_iterator ctr = controllers.find(id);
if(ctr != controllers.end()) {
if (const config& c = gamestate.snapshot.find_child("side", "save_id", id)) {
side["current_player"] = c["current_player"];
}
side["controller"] = ctr->second.controller;
}
}
if (side["controller"].empty())
side["controller"] = "ai";
}
// If the entire scenario should be randomly generated
if((*scenario)["scenario_generation"] != "") {
LOG_G << "randomly generating scenario...\n";
const cursor::setter cursor_setter(cursor::WAIT);
static config scenario2;
scenario2 = random_generate_scenario((*scenario)["scenario_generation"], scenario->child("generator"));
//TODO comment or remove
//level_ = scenario;
scenario = &scenario2;
}
std::string map_data = (*scenario)["map_data"];
if(map_data.empty() && (*scenario)["map"] != "") {
map_data = read_map((*scenario)["map"]);
}
// If the map should be randomly generated
if(map_data.empty() && (*scenario)["map_generation"] != "") {
const cursor::setter cursor_setter(cursor::WAIT);
map_data = random_generate_map((*scenario)["map_generation"],scenario->child("generator"));
// Since we've had to generate the map,
// make sure that when we save the game,
// it will not ask for the map to be generated again on reload
static config new_level;
new_level = *scenario;
new_level["map_data"] = map_data;
scenario = &new_level;
LOG_G << "generated map\n";
}
// Sends scenario data
config cfg;
config& next_cfg = cfg.add_child("store_next_scenario", *scenario);
// Adds player information, and other state
// information, to the configuration object
gamestate.write_snapshot(next_cfg);
next_cfg["next_scenario"] = (*scenario)["next_scenario"];
next_cfg.add_child("snapshot");
//move the player information into the hosts gamestate
write_players(gamestate, starting_pos, true, true);
next_cfg["random_seed"] = gamestate.carryover_sides_start["random_seed"];
next_cfg["random_calls"] = gamestate.carryover_sides_start["random_calls"];
next_cfg.add_child("variables", gamestate.carryover_sides_start.child("variables"));
next_cfg.add_child("multiplayer", gamestate.mp_settings().to_config());
//Merge in-game information from carryover_sides_start with scenario to create replay_start
next_cfg.add_child("replay_start", *scenario);
config gamedata;
game_data(gamestate.carryover_sides_start).write_snapshot(gamedata);
next_cfg.child("replay_start").merge_with(gamedata);
//move side information from gamestate into the config that is sent to the other clients
next_cfg.clear_children("side");
BOOST_FOREACH(config& side, starting_pos.child_range("side")){
next_cfg.add_child("side", side);
}
network::send_data(cfg, 0);
}
}
if(scenario != NULL) {
// Update the label
if (gamestate.classification().abbrev.empty())
gamestate.classification().label = (*scenario)["name"].str();
else {
gamestate.classification().label = gamestate.classification().abbrev;
gamestate.classification().label.append("-");
gamestate.classification().label.append((*scenario)["name"]);
}
// If this isn't the last scenario, then save the game
if(save_game_after_scenario) {
// For multiplayer, we want the save
// to contain the starting position.
// For campaigns however, this is the
// start-of-scenario save and the
// starting position needs to be empty,
// to force a reload of the scenario config.
savegame::scenariostart_savegame save(gamestate, preferences::compress_saves());
save.save_game_automatic(disp.video());
}
}
gamestate.snapshot = config();
}
if (!gamestate.carryover_sides_start["next_scenario"].empty() && gamestate.carryover_sides_start["next_scenario"] != "null") {
std::string message = _("Unknown scenario: '$scenario|'");
utils::string_map symbols;
symbols["scenario"] = gamestate.carryover_sides_start["next_scenario"];
message = utils::interpolate_variables_into_string(message, &symbols);
gui2::show_error_message(disp.video(), message);
return QUIT;
}
if (gamestate.classification().campaign_type == "scenario"){
if (preferences::delete_saves())
savegame::clean_saves(gamestate.classification().label);
}
return VICTORY;
}