wesnoth/src/replay.cpp
2014-12-21 18:57:25 -03:00

1214 lines
33 KiB
C++

/*
Copyright (C) 2003 - 2014 by David White <dave@whitevine.net>
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
* Replay control code.
*
* See http://www.wesnoth.org/wiki/ReplayWML for more info.
*/
#include "global.hpp"
#include "replay.hpp"
#include "actions/undo.hpp"
#include "config_assign.hpp"
#include "dialogs.hpp"
#include "display_chat_manager.hpp"
#include "game_display.hpp"
#include "game_preferences.hpp"
#include "game_data.hpp"
#include "log.hpp"
#include "map_label.hpp"
#include "map_location.hpp"
#include "play_controller.hpp"
#include "synced_context.hpp"
#include "resources.hpp"
#include "statistics.hpp"
#include "unit.hpp"
#include "whiteboard/manager.hpp"
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>
#include <set>
#include <map>
static lg::log_domain log_replay("replay");
#define DBG_REPLAY LOG_STREAM(debug, log_replay)
#define LOG_REPLAY LOG_STREAM(info, log_replay)
#define WRN_REPLAY LOG_STREAM(warn, log_replay)
#define ERR_REPLAY LOG_STREAM(err, log_replay)
static lg::log_domain log_random("random");
#define DBG_RND LOG_STREAM(debug, log_random)
#define LOG_RND LOG_STREAM(info, log_random)
#define WRN_RND LOG_STREAM(warn, log_random)
#define ERR_RND LOG_STREAM(err, log_random)
//functions to verify that the unit structure on both machines is identical
static void verify(const unit_map& units, const config& cfg) {
std::stringstream errbuf;
LOG_REPLAY << "verifying unit structure...\n";
const size_t nunits = cfg["num_units"].to_size_t();
if(nunits != units.size()) {
errbuf << "SYNC VERIFICATION FAILED: number of units from data source differ: "
<< nunits << " according to data source. " << units.size() << " locally\n";
std::set<map_location> locs;
BOOST_FOREACH(const config &u, cfg.child_range("unit"))
{
const map_location loc(u, resources::gamedata);
locs.insert(loc);
if(units.count(loc) == 0) {
errbuf << "data source says there is a unit at "
<< loc << " but none found locally\n";
}
}
for(unit_map::const_iterator j = units.begin(); j != units.end(); ++j) {
if (locs.count(j->get_location()) == 0) {
errbuf << "local unit at " << j->get_location()
<< " but none in data source\n";
}
}
replay::process_error(errbuf.str());
errbuf.clear();
}
BOOST_FOREACH(const config &un, cfg.child_range("unit"))
{
const map_location loc(un, resources::gamedata);
const unit_map::const_iterator u = units.find(loc);
if(u == units.end()) {
errbuf << "SYNC VERIFICATION FAILED: data source says there is a '"
<< un["type"] << "' (side " << un["side"] << ") at "
<< loc << " but there is no local record of it\n";
replay::process_error(errbuf.str());
errbuf.clear();
}
config cfg;
u->write(cfg);
bool is_ok = true;
static const std::string fields[] = {"type","hitpoints","experience","side",""};
for(const std::string* str = fields; str->empty() == false; ++str) {
if (cfg[*str] != un[*str]) {
errbuf << "ERROR IN FIELD '" << *str << "' for unit at "
<< loc << " data source: '" << un[*str]
<< "' local: '" << cfg[*str] << "'\n";
is_ok = false;
}
}
if(!is_ok) {
errbuf << "(SYNC VERIFICATION FAILED)\n";
replay::process_error(errbuf.str());
errbuf.clear();
}
}
LOG_REPLAY << "verification passed\n";
}
static time_t get_time(const config &speak)
{
time_t time;
if (!speak["time"].empty())
{
std::stringstream ss(speak["time"].str());
ss >> time;
}
else
{
//fallback in case sender uses wesnoth that doesn't send timestamps
time = ::time(NULL);
}
return time;
}
// FIXME: this one now has to be assigned with set_random_generator
// from play_level or similar. We should surely hunt direct
// references to it from this very file and move it out of here.
replay recorder;
chat_msg::chat_msg(const config &cfg)
: color_()
, nick_()
, text_(cfg["message"].str())
{
const std::string& team_name = cfg["team_name"];
if(team_name == "")
{
nick_ = cfg["id"].str();
} else {
nick_ = str_cast("*")+cfg["id"].str()+"*";
}
int side = cfg["side"].to_int(0);
LOG_REPLAY << "side in message: " << side << std::endl;
if (side==0) {
color_ = "white";//observers
} else {
color_ = team::get_side_highlight_pango(side-1);
}
time_ = get_time(cfg);
/*
} else if (side==1) {
color_ = "red";
} else if (side==2) {
color_ = "blue";
} else if (side==3) {
color_ = "green";
} else if (side==4) {
color_ = "purple";
}*/
}
chat_msg::~chat_msg()
{
}
replay::replay() :
cfg_(),
pos_(0),
skip_(false),
message_locations()
{}
replay::replay(const config& cfg) :
cfg_(cfg),
pos_(0),
skip_(false),
message_locations()
{}
void replay::append(const config& cfg)
{
cfg_.append(cfg);
}
/*
TODO: there should be different types of OOS messages:
1)the normal OOS message
2) the 'is guarenteed you'll get an assertion error after this and therefore you cannot continur' OOS message
3) the 'do you want to overwrite calculated data with the data stored in replay' OOS error message.
*/
void replay::process_error(const std::string& msg)
{
ERR_REPLAY << msg;
resources::controller->process_oos(msg); // might throw end_level_exception(QUIT)
}
void replay::set_skip(bool skip)
{
skip_ = skip;
}
bool replay::is_skipping() const
{
return skip_;
}
void replay::add_unit_checksum(const map_location& loc,config& cfg)
{
if(! game_config::mp_debug) {
return;
}
config& cc = cfg.add_child("checksum");
loc.write(cc);
unit_map::const_iterator u = resources::units->find(loc);
assert(u.valid());
cc["value"] = get_checksum(*u);
}
void replay::init_side()
{
config& cmd = add_command();
config init_side;
init_side["side_number"] = resources::controller->current_side();
cmd.add_child("init_side", init_side);
}
void replay::add_start()
{
config& cmd = add_command();
cmd["sent"] = true;
cmd.add_child("start");
}
void replay::add_countdown_update(int value, int team)
{
config& cmd = add_command();
config val;
val["value"] = value;
val["team"] = team;
cmd.add_child("countdown_update",val);
}
void replay::add_synced_command(const std::string& name, const config& command)
{
config& cmd = add_command();
cmd.add_child(name,command);
LOG_REPLAY << "add_synced_command: \n" << cmd.debug() << "\n";
}
void replay::user_input(const std::string &name, const config &input, int from_side)
{
config& cmd = add_command();
cmd["dependent"] = true;
if(from_side == -1)
{
cmd["from_side"] = "server";
}
else
{
cmd["from_side"] = from_side;
}
cmd.add_child(name, input);
}
void replay::add_label(const terrain_label* label)
{
assert(label);
config& cmd = add_nonundoable_command();
config val;
label->write(val);
cmd.add_child("label",val);
}
void replay::clear_labels(const std::string& team_name, bool force)
{
config& cmd = add_nonundoable_command();
config val;
val["team_name"] = team_name;
val["force"] = force;
cmd.add_child("clear_labels",val);
}
void replay::add_rename(const std::string& name, const map_location& loc)
{
config& cmd = add_command();
cmd["async"] = true; // Not undoable, but depends on moves/recruits that are
config val;
loc.write(val);
val["name"] = name;
cmd.add_child("rename", val);
}
void replay::end_turn()
{
config& cmd = add_command();
cmd.add_child("end_turn");
}
void replay::add_log_data(const std::string &key, const std::string &var)
{
config& ulog = cfg_.child_or_add("upload_log");
ulog[key] = var;
}
void replay::add_log_data(const std::string &category, const std::string &key, const std::string &var)
{
config& ulog = cfg_.child_or_add("upload_log");
config& cat = ulog.child_or_add(category);
cat[key] = var;
}
void replay::add_log_data(const std::string &category, const std::string &key, const config &c)
{
config& ulog = cfg_.child_or_add("upload_log");
config& cat = ulog.child_or_add(category);
cat.add_child(key,c);
}
void replay::add_checksum_check(const map_location& loc)
{
if(! game_config::mp_debug || ! (resources::units->find(loc).valid()) ) {
return;
}
config& cmd = add_command();
cmd["dependent"] = true;
add_unit_checksum(loc,cmd);
}
void replay::add_chat_message_location()
{
message_locations.push_back(pos_-1);
}
void replay::speak(const config& cfg)
{
config& cmd = add_nonundoable_command();
cmd.add_child("speak",cfg);
add_chat_message_location();
}
void replay::add_chat_log_entry(const config &cfg, std::back_insert_iterator<std::vector<chat_msg> > &i) const
{
if (!cfg) return;
if (!preferences::parse_should_show_lobby_join(cfg["id"], cfg["message"])) return;
if (preferences::is_ignored(cfg["id"])) return;
*i = chat_msg(cfg);
}
void replay::remove_command(int index)
{
cfg_.remove_child("command", index);
std::vector<int>::reverse_iterator loc_it;
for (loc_it = message_locations.rbegin(); loc_it != message_locations.rend() && index < *loc_it;++loc_it)
{
--(*loc_it);
}
}
// cached message log
static std::vector< chat_msg > message_log;
const std::vector<chat_msg>& replay::build_chat_log()
{
std::vector<int>::iterator loc_it;
int last_location = 0;
std::back_insert_iterator<std::vector < chat_msg > > chat_log_appender( back_inserter(message_log));
for (loc_it = message_locations.begin(); loc_it != message_locations.end(); ++loc_it)
{
last_location = *loc_it;
const config &speak = command(last_location).child("speak");
assert(speak);
add_chat_log_entry(speak, chat_log_appender);
}
message_locations.clear();
return message_log;
}
config replay::get_data_range(int cmd_start, int cmd_end, DATA_TYPE data_type)
{
config res;
for (int cmd = cmd_start; cmd < cmd_end; ++cmd)
{
config &c = command(cmd);
//prevent creating 'blank' attribute values during checks
const config &cc = c;
if ((data_type == ALL_DATA || !cc["undo"].to_bool(true)) && !cc["sent"].to_bool(false))
{
res.add_child("command", c);
if (data_type == NON_UNDO_DATA) c["sent"] = true;
}
}
return res;
}
struct async_cmd
{
config *cfg;
int num;
};
void replay::redo(const config& cfg)
{
//we set pos_ = ncommands(), if we recorded something else in the meantime it doesn't make sense to redo an action.
assert(pos_ == ncommands());
BOOST_FOREACH(const config &cmd, cfg.child_range("command"))
{
/*config &cfg = */cfg_.add_child("command", cmd);
}
pos_ = ncommands();
}
config& replay::get_last_real_command()
{
for (int cmd_num = pos_ - 1; cmd_num >= 0; --cmd_num)
{
config &c = command(cmd_num);
const config &cc = c;
if (cc["dependent"].to_bool(false) || !cc["undo"].to_bool(true) || cc["async"].to_bool(false))
{
continue;
}
return c;
}
ERR_REPLAY << "replay::get_last_real_command called with no existent command." << std::endl;
assert(false && "replay::get_last_real_command called with no existent command.");
throw "replay::get_last_real_command called with no existent command.";
}
void replay::undo_cut(config& dst)
{
assert(dst.empty());
//pos_ < ncommands() could mean that we try to undo commands that haven't been executed yet.
assert(pos_ == ncommands());
std::vector<async_cmd> async_cmds;
// Remember commands not yet synced and skip over them.
// We assume that all already sent (sent=yes) data isn't undoable
// even if not marked explicitly with undo=no.
/**
* @todo Change undo= to default to "no" and explicitly mark all
* undoable commands with yes.
*/
int cmd;
for (cmd = ncommands() - 1; cmd >= 0; --cmd)
{
//"undo"=no means speak/label/remove_label, especialy attack, recruits etc. have "undo"=yes
//"async"=yes means rename_unit
//"dependent"=true means user input or unit_checksum_check
config &c = command(cmd);
const config &cc = c;
if (cc["dependent"].to_bool(false))
{
continue;
}
if (cc["undo"].to_bool(true) && !cc["async"].to_bool(false) && !cc["sent"].to_bool(false)) break;
if (cc["async"].to_bool(false)) {
async_cmd ac = { &c, cmd };
async_cmds.push_back(ac);
}
}
if (cmd < 0) return;
//we add the commands that we want to remove later to the passed cfg first.
dst.add_child("command", cfg_.child("command", cmd));
//we do this in a seperate loop because we don't want to loop forward in the loop while when we remove the elements to keepo the indexes simple.
for(int cmd_2 = cmd + 1; cmd_2 < ncommands(); ++cmd_2)
{
if(command(cmd_2)["dependent"].to_bool(false))
{
dst.add_child("command", cfg_.child("command", cmd_2));
}
}
//we remove dependent commands after the actual removed command that don't make sense if they stand alone especialy user choices and checksum data.
for(int cmd_2 = ncommands() - 1; cmd_2 > cmd; --cmd_2)
{
if(command(cmd_2)["dependent"].to_bool(false))
{
remove_command(cmd_2);
}
}
config &c = command(cmd);
if (const config &child = c.child("move"))
{
// A unit's move is being undone.
// Repair unsynced cmds whose locations depend on that unit's location.
std::vector<map_location> steps;
try {
read_locations(child,steps);
} catch (bad_lexical_cast &) {
WRN_REPLAY << "Warning: Path data contained something which could not be parsed to a sequence of locations:" << "\n config = " << child.debug() << std::endl;
}
if (steps.empty()) {
ERR_REPLAY << "trying to undo a move using an empty path";
}
else {
const map_location early_stop(child["stop_x"].to_int(-999) - 1,
child["stop_y"].to_int(-999) - 1);
const map_location &src = steps.front();
const map_location &dst = early_stop.valid() ? early_stop : steps.back();
BOOST_FOREACH(const async_cmd &ac, async_cmds)
{
if (config &async_child = ac.cfg->child("rename")) {
map_location aloc(async_child, resources::gamedata);
if (dst == aloc) src.write(async_child);
}
}
}
}
else
{
const config *chld = &c.child("recruit");
if (!*chld) chld = &c.child("recall");
if (*chld) {
// A unit is being un-recruited or un-recalled.
// Remove unsynced commands that would act on that unit.
map_location src(*chld, resources::gamedata);
BOOST_FOREACH(const async_cmd &ac, async_cmds)
{
if (config &async_child = ac.cfg->child("rename"))
{
map_location aloc(async_child, resources::gamedata);
if (src == aloc) remove_command(ac.num);
}
}
}
}
remove_command(cmd);
pos_ = ncommands();
}
void replay::undo()
{
config dummy;
undo_cut(dummy);
}
config &replay::command(int n)
{
config & retv = cfg_.child("command", n);
assert(retv);
return retv;
}
int replay::ncommands() const
{
return cfg_.child_count("command");
}
config& replay::add_command()
{
//pos_ != ncommands() means that there is a command on the replay which would be skipped.
assert(pos_ == ncommands());
pos_ = ncommands()+1;
return cfg_.add_child("command");
}
config& replay::add_nonundoable_command()
{
config& r = cfg_.add_child_at("command",config(), pos_);
r["undo"] = false;
++pos_;
return r;
}
void replay::start_replay()
{
pos_ = 0;
}
void replay::revert_action()
{
if (pos_ > 0)
--pos_;
}
config* replay::get_next_action()
{
if (pos_ >= ncommands())
return NULL;
LOG_REPLAY << "up to replay action " << pos_ + 1 << '/' << ncommands() << '\n';
config* retv = &command(pos_);
++pos_;
return retv;
}
bool replay::at_end() const
{
return pos_ >= ncommands();
}
void replay::set_to_end()
{
pos_ = ncommands();
}
void replay::clear()
{
message_locations.clear();
message_log.clear();
cfg_ = config();
pos_ = 0;
skip_ = false;
}
bool replay::empty()
{
return ncommands() == 0;
}
void replay::add_config(const config& cfg, MARK_SENT mark)
{
BOOST_FOREACH(const config &cmd, cfg.child_range("command"))
{
config &cfg = cfg_.add_child("command", cmd);
if(mark == MARK_AS_SENT) {
cfg["sent"] = true;
}
}
}
replay& get_replay_source()
{
return recorder;
}
static void check_checksums(const config &cfg)
{
if(! game_config::mp_debug) {
return;
}
BOOST_FOREACH(const config &ch, cfg.child_range("checksum"))
{
map_location loc(ch, resources::gamedata);
unit_map::const_iterator u = resources::units->find(loc);
if (!u.valid()) {
std::stringstream message;
message << "non existent unit to checksum at " << loc.x+1 << "," << loc.y+1 << "!";
resources::screen->get_chat_manager().add_chat_message(time(NULL), "verification", 1, message.str(),
events::chat_handler::MESSAGE_PRIVATE, false);
continue;
}
if (get_checksum(*u) != ch["value"]) {
std::stringstream message;
message << "checksum mismatch at " << loc.x+1 << "," << loc.y+1 << "!";
resources::screen->get_chat_manager().add_chat_message(time(NULL), "verification", 1, message.str(),
events::chat_handler::MESSAGE_PRIVATE, false);
}
}
}
bool replay::add_start_if_not_there_yet()
{
//this method would confuse the value of 'pos' otherwise
assert(pos_ == 0);
if(at_end() || !cfg_.child("command", pos_).has_child("start"))
{
cfg_.add_child_at("command",config_of("start", config()),pos_);
return true;
}
else
{
return false;
}
}
static void show_oos_error_error_function(const std::string& message, bool /*heavy*/)
{
replay::process_error(message);
}
REPLAY_RETURN do_replay(bool one_move)
{
log_scope("do replay");
if (!get_replay_source().is_skipping()){
resources::screen->recalculate_minimap();
}
update_locker lock_update(resources::screen->video(),get_replay_source().is_skipping());
return do_replay_handle(one_move);
}
REPLAY_RETURN do_replay_handle(bool one_move)
{
//team &current_team = (*resources::teams)[side_num - 1];
const int side_num = resources::controller->current_side();
for(;;) {
const config *cfg = get_replay_source().get_next_action();
const bool is_synced = (synced_context::get_synced_state() == synced_context::SYNCED);
DBG_REPLAY << "in do replay with is_synced=" << is_synced << "\n";
if (cfg != NULL)
{
DBG_REPLAY << "Replay data:\n" << *cfg << "\n";
}
else
{
DBG_REPLAY << "Replay data at end\n";
return REPLAY_RETURN_AT_END;
}
const config::all_children_itors ch_itors = cfg->all_children_range();
//if there is an empty command tag or a start tag
if (ch_itors.first == ch_itors.second || cfg->has_child("start"))
{
//this shouldn't happen anymore becasue replaycontroller now moves over the [start] with get_next_action
//also we removed the the "add empty replay entry at scenario reload" behaviour.
ERR_REPLAY << "found "<< cfg->debug() <<" in replay" << std::endl;
//do nothing
}
else if (const config &child = cfg->child("speak"))
{
const std::string &team_name = child["team_name"];
const std::string &speaker_name = child["id"];
const std::string &message = child["message"];
//if (!preferences::parse_should_show_lobby_join(speaker_name, message)) return;
bool is_whisper = (speaker_name.find("whisper: ") == 0);
get_replay_source().add_chat_message_location();
if (!get_replay_source().is_skipping() || is_whisper) {
int side = child["side"];
resources::screen->get_chat_manager().add_chat_message(get_time(child), speaker_name, side, message,
(team_name.empty() ? events::chat_handler::MESSAGE_PUBLIC
: events::chat_handler::MESSAGE_PRIVATE),
preferences::message_bell());
}
}
else if (const config &child = cfg->child("label"))
{
terrain_label label(resources::screen->labels(), child);
resources::screen->labels().set_label(label.location(),
label.text(),
label.team_name(),
label.color());
}
else if (const config &child = cfg->child("clear_labels"))
{
resources::screen->labels().clear(std::string(child["team_name"]), child["force"].to_bool());
}
else if (const config &child = cfg->child("rename"))
{
const map_location loc(child, resources::gamedata);
const std::string &name = child["name"];
unit_map::iterator u = resources::units->find(loc);
if (u.valid()) {
if (u->unrenamable()) {
std::stringstream errbuf;
errbuf << "renaming unrenamable unit " << u->id() << '\n';
replay::process_error(errbuf.str());
continue;
}
u->rename(name);
} else {
// Users can rename units while it's being killed at another machine.
// This since the player can rename units when it's not his/her turn.
// There's not a simple way to prevent that so in that case ignore the
// rename instead of throwing an OOS.
WRN_REPLAY << "attempt to rename unit at location: "
<< loc << ", where none exists (anymore).\n";
}
}
else if (cfg->child("init_side"))
{
if(is_synced)
{
replay::process_error("found init_side in replay while is_synced=true\n" );
get_replay_source().revert_action();
//fits better than the other options, and should have the desired effect.
return REPLAY_FOUND_DEPENDENT;
}
else
{
set_scontext_synced sync;
resources::controller->do_init_side(true);
}
}
//if there is an end turn directive
else if (cfg->child("end_turn"))
{
if(is_synced)
{
replay::process_error("found end_turn in replay while is_synced=true\n" );
get_replay_source().revert_action();
//fits better than the other options, and should have the desired effect.
return REPLAY_FOUND_DEPENDENT;
}
else
{
// During the original game, the undo stack would have been
// committed at this point.
resources::undo_stack->clear();
if (const config &child = cfg->child("verify")) {
verify(*resources::units, child);
}
return REPLAY_FOUND_END_TURN;
}
}
else if (const config &child = cfg->child("countdown_update"))
{
int val = child["value"];
int tval = child["team"];
if (tval <= 0 || tval > int(resources::teams->size())) {
std::stringstream errbuf;
errbuf << "Illegal countdown update \n"
<< "Received update for :" << tval << " Current user :"
<< side_num << "\n" << " Updated value :" << val;
replay::process_error(errbuf.str());
} else {
(*resources::teams)[tval - 1].set_countdown_time(val);
}
}
else if ( cfg->child("checksum") )
{
check_checksums(*cfg);
}
else if ((*cfg)["dependent"].to_bool(false))
{
if(!is_synced)
{
replay::process_error("found dependent command in replay while is_synced=false\n" );
//ignore this command
continue;
}
//this means user choice.
// it never makes sense to try to execute a user choice.
// but we are called from
// the only other option for "dependent" command is checksum wich is already checked.
assert(cfg->all_children_count() == 1);
std::string child_name = cfg->all_children_range().first->key;
DBG_REPLAY << "got an dependent action name = " << child_name <<"\n";
get_replay_source().revert_action();
return REPLAY_FOUND_DEPENDENT;
}
else
{
//we checked for empty commands at the beginning.
const std::string & commandname = cfg->ordered_begin()->key;
config data = cfg->ordered_begin()->cfg;
if(is_synced)
{
replay::process_error("found " + commandname + " command in replay while is_synced=true\n" );
get_replay_source().revert_action();
//fits better than the other options, and should have the desired effect.
return REPLAY_FOUND_DEPENDENT;
}
else
{
LOG_REPLAY << "found commandname " << commandname << "in replay";
/*
we need to use the undo stack during replays in order to make delayed shroud updated work.
*/
synced_context::run_in_synced_context(commandname, data, true, !get_replay_source().is_skipping(), false,show_oos_error_error_function);
if (one_move) {
return REPLAY_FOUND_END_MOVE;
}
}
}
if (const config &child = cfg->child("verify")) {
verify(*resources::units, child);
}
}
}
replay_network_sender::replay_network_sender(replay& obj) : obj_(obj), upto_(obj_.ncommands())
{
}
replay_network_sender::~replay_network_sender()
{
try {
commit_and_sync();
} catch (...) {}
}
void replay_network_sender::sync_non_undoable()
{
if(network::nconnections() > 0) {
resources::whiteboard->send_network_data();
config cfg;
const config& data = cfg.add_child("turn",obj_.get_data_range(upto_,obj_.ncommands(),replay::NON_UNDO_DATA));
if(data.empty() == false) {
network::send_data(cfg, 0);
}
}
}
void replay_network_sender::commit_and_sync()
{
if(network::nconnections() > 0) {
resources::whiteboard->send_network_data();
config cfg;
const config& data = cfg.add_child("turn",obj_.get_data_range(upto_,obj_.ncommands()));
if(data.empty() == false) {
network::send_data(cfg, 0);
}
upto_ = obj_.ncommands();
}
}
static std::map<int, config> get_user_choice_internal(const std::string &name, const mp_sync::user_choice &uch, const std::set<int>& sides)
{
const int max_side = static_cast<int>(resources::teams->size());
BOOST_FOREACH(int side, sides)
{
//the caller has to ensure this.
assert(1 <= side && side <= max_side);
assert(!(*resources::teams)[side-1].is_empty());
}
//this should never change during the execution of this function.
const int current_side = resources::controller->current_side();
const bool is_mp_game = network::nconnections() != 0;
std::map<int,config> retv;
/*
when we got all our answers we stop.
*/
while(retv.size() != sides.size())
{
/*
there might be speak or similar commands in the replay before the user input.
*/
do_replay_handle();
/*
these value might change due to player left/reassign during pull_remote_user_input
*/
//equals to any side in sides that is local, 0 if no such side exists.
int local_side = 0;
//if for any side from which we need an answer
BOOST_FOREACH(int side, sides)
{
//and we havent already received our answer from that side
if(retv.find(side) == retv.end())
{
//and it is local
if((*resources::teams)[side-1].is_local() && !(*resources::teams)[side-1].is_idle())
{
//then we have to make a local choice.
local_side = side;
break;
}
}
}
bool has_local_side = local_side != 0;
bool is_replay_end = get_replay_source().at_end();
if (is_replay_end && has_local_side)
{
set_scontext_local_choice sync;
/* At least one of the decisions is ours, and it will be inserted
into the replay. */
DBG_REPLAY << "MP synchronization: local choice\n";
config cfg = uch.query_user(local_side);
recorder.user_input(name, cfg, local_side);
retv[local_side]= cfg;
//send data to others.
//but if there wasn't any data sended during this turn, we don't want to bein wth that now.
if(synced_context::is_simultaneously() || current_side != local_side)
{
synced_context::send_user_choice();
}
continue;
}
else if(is_replay_end && !has_local_side)
{
//we are in a mp game, and the data has not been recieved yet.
DBG_REPLAY << "MP synchronization: waiting for remote choice\n";
assert(is_mp_game);
synced_context::pull_remote_user_input();
SDL_Delay(10);
continue;
}
else if(!is_replay_end)
{
DBG_REPLAY << "MP synchronization: extracting choice from replay with has_local_side=" << has_local_side << "\n";
const config *action = get_replay_source().get_next_action();
assert(action); //action cannot be null because get_replay_source().at_end() returned false.
if( !action->has_child(name))
{
replay::process_error("[" + name + "] expected but none found\n. found instead:\n" + action->debug());
//We save this action for later
get_replay_source().revert_action();
//and let the user try to get the intended result.
BOOST_FOREACH(int side, sides)
{
if(retv.find(side) == retv.end())
{
retv[side] = uch.query_user(side);
}
}
return retv;
}
int from_side = (*action)["from_side"].to_int(0);
if ((*action)["side_invalid"].to_bool(false) == true)
{
//since this 'cheat' can have a quite heavy effect especialy in umc content we give an oos error .
replay::process_error("MP synchronization: side_invalid in replay data, this could mean someone wants to cheat.\n");
}
if (sides.find(from_side) == sides.end())
{
replay::process_error("MP synchronization: we got an answer from side " + boost::lexical_cast<std::string>(from_side) + "for [" + name + "] which is not was we expected\n");
continue;
}
if(retv.find(from_side) != retv.end())
{
replay::process_error("MP synchronization: we got already our answer from side " + boost::lexical_cast<std::string>(from_side) + "for [" + name + "] now we have it twice.\n");
}
retv[from_side] = action->child(name);
continue;
}
}//while
return retv;
}
std::map<int,config> mp_sync::get_user_choice_multiple_sides(const std::string &name, const mp_sync::user_choice &uch,
std::set<int> sides)
{
//pass sides by copy because we need a copy.
const bool is_synced = synced_context::get_synced_state() == synced_context::SYNCED;
const int max_side = static_cast<int>(resources::teams->size());
//we currently don't check for too early because luas sync choice doesn't necessarily show screen dialogs.
//It (currently) in the responsibility of the user of sync choice to not use dialogs during prestart events..
if(!is_synced)
{
//we got called from inside luas wesnoth.synchronize_choice or from a select event.
replay::process_error("MP synchronization only works in a synced context (for example Select or preload events are no synced context).\n");
return std::map<int,config>();
}
/*
for empty sides we want to use random choice instead.
*/
std::set<int> empty_sides;
BOOST_FOREACH(int side, sides)
{
assert(1 <= side && side <= max_side);
if( (*resources::teams)[side-1].is_empty())
{
empty_sides.insert(side);
}
}
BOOST_FOREACH(int side, empty_sides)
{
sides.erase(side);
}
std::map<int,config> retv = get_user_choice_internal(name, uch, sides);
BOOST_FOREACH(int side, empty_sides)
{
retv[side] = uch.random_choice(side);
}
return retv;
}
/*
fixes some rare cases and calls get_user_choice_internal if we are in a synced context.
*/
config mp_sync::get_user_choice(const std::string &name, const mp_sync::user_choice &uch,
int side)
{
const bool is_too_early = resources::gamedata->phase() != game_data::START && resources::gamedata->phase() != game_data::PLAY;
const bool is_synced = synced_context::get_synced_state() == synced_context::SYNCED;
const bool is_mp_game = network::nconnections() != 0;//Only used in debugging output below
const int max_side = static_cast<int>(resources::teams->size());
bool is_side_null_controlled;
if(!is_synced)
{
//we got called from inside luas wesnoth.synchronize_choice or from a select event (or maybe a preload event?).
//This doesn't cause problems and someone could use it for example to use a [message][option] inside a wesnoth.synchronize_choice which could be useful,
//so just give a warning.
WRN_REPLAY << "MP synchronization called during an unsynced context.\n";
return uch.query_user(side);
}
if(is_too_early && uch.is_visible())
{
//We are in a prestart event or even earlier.
//Although we are able to sync them, we cannot use query_user,
//because we cannot (or shouldn't) put things on the screen inside a prestart event, this is true for SP and MP games.
//Quotation form event wiki: "For things displayed on-screen such as character dialog, use start instead"
return uch.random_choice(side);
}
//in start events it's unclear to decide on which side the function should be executed (default= side1 still).
//But for advancements we can just decide on the side that owns the unit and that's in the responsibility of advance_unit_at.
//For [message][option] and luas sync_choice the scenario designer is responsible for that.
//For [get_global_variable] side is never null.
/*
side = 0 should default to the currently active side per definition.
*/
if ( side < 1 || max_side < side )
{
if(side != 0)
{
ERR_REPLAY << "Invalid parameter for side in get_user_choice." << std::endl;
}
side = resources::controller->current_side();
LOG_REPLAY << " side changed to " << side << "\n";
}
is_side_null_controlled = (*resources::teams)[side-1].is_empty();
LOG_REPLAY << "get_user_choice_called with"
<< " name=" << name
<< " is_synced=" << is_synced
<< " is_mp_game=" << is_mp_game
<< " is_side_null_controlled=" << is_side_null_controlled << "\n";
if (is_side_null_controlled)
{
DBG_REPLAY << "MP synchronization: side 1 being null-controlled in get_user_choice.\n";
//most likely we are in a start event with an empty side 1
//but calling [set_global_variable] to an empty side might also cause this.
//i think in that case we should better use uch.random_choice(),
//which could return something like config_of("invalid", true);
side = 1;
while ( side <= max_side && (*resources::teams)[side-1].is_empty() )
side++;
assert(side <= max_side);
}
assert(1 <= side && side <= max_side);
std::set<int> sides;
sides.insert(side);
std::map<int, config> retv = get_user_choice_internal(name, uch, sides);
if(retv.find(side) == retv.end())
{
//An error occured, get_user_choice_internal should have given an oos error message
return config();
}
return retv[side];
}