add synced_context class

the intention  is to fix #20871 and implement #21697, to sync
prestart/start events, and to implement
http://forums.wesnoth.org/viewtopic.php?f=6&t=39611 in the next patches,

the intention of synced_checkup is to replace random .. set/get_random_results.
Because set/get_random_results didn't have much to do with random, since it was only used to compare unit checksums and attacker damage to replays.

the intention of synced_commands is to move code out of do_replay_handle and make it callable from other places too so that we can just call the same function taht was called from replay in the simple cases (with synced_context::run_in_synced_command).

the object set_scontext_synced can be used to enter the synced context which enables synced_checkup and make the random calls synced.
Or we can use run_in_synced_context which is normaly easier than set_scontext_synced.
we can check wether we are in a synced context with get_syced_state to make addidional checks to detect oos (the other intention of that is to implement #21697 ).

this commit is part of pf 121.
This commit is contained in:
gfgtdf 2014-03-19 17:17:56 +01:00
parent 8f00786fb6
commit fc8c15e46a
8 changed files with 1316 additions and 1 deletions

View file

@ -868,6 +868,9 @@ set(wesnoth-main_SRC
storyscreen/part.cpp
storyscreen/render.cpp
strftime.cpp
synced_checkup.cpp
synced_context.cpp
synced_commands.cpp
team.cpp
terrain_filter.cpp
tod_manager.cpp

View file

@ -501,7 +501,10 @@ wesnoth_sources = Split("""
storyscreen/interface.cpp
storyscreen/part.cpp
storyscreen/render.cpp
strftime.cpp
strftime.cpp
synced_checkup.cpp
synced_context.cpp
synced_commands.cpp
team.cpp
terrain_filter.cpp
tod_manager.cpp

133
src/synced_checkup.cpp Normal file
View file

@ -0,0 +1,133 @@
/*
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.
*/
#include "synced_checkup.hpp"
#include "log.hpp"
#include "map_location.hpp"
#include "unit_map.hpp"
#include "unit.hpp"
#include "resources.hpp"
#include "game_display.hpp"
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)
ignored_checkup default_instnce;
checkup* checkup_instance = &default_instnce;
checkup::checkup()
{
}
checkup::~checkup()
{
}
void checkup::unit_checksum(const map_location& loc, bool local)
{
unit_map::iterator u = resources::units->find(loc);
bool equals;
config real;
config expected;
if (!u.valid()) {
std::stringstream message;
message << "non existent unit to checksum at " << loc.x+1 << "," << loc.y+1 << "!";
resources::screen->add_chat_message(time(NULL), "verification", 1, message.str(),
events::chat_handler::MESSAGE_PRIVATE, false);
}
else
{
expected["checksum"] = get_checksum(*u);
}
if(local)
{
equals = local_checkup(expected, real);
}
else
{
equals = this->networked_checkup(expected, real);
}
if(!equals && ((game_config::mp_debug && !local) || local))
{
std::stringstream message;
message << "checksum mismatch at " << loc.x+1 << "," << loc.y+1 << "!";
resources::screen->add_chat_message(time(NULL), "verification", 1, message.str(),
events::chat_handler::MESSAGE_PRIVATE, false);
}
}
ignored_checkup::ignored_checkup()
{
}
ignored_checkup::~ignored_checkup()
{
}
bool ignored_checkup::local_checkup(const config& /*expected_data*/, config& real_data)
{
assert(real_data.empty());
LOG_REPLAY << "ignored_checkup::local_checkup called\n";
return true;
}
bool ignored_checkup::networked_checkup(const config& /*expected_data*/, config& real_data)
{
assert(real_data.empty());
LOG_REPLAY << "ignored_checkup::networked_checkup called\n";
return true;
}
synced_checkup::synced_checkup(config& buffer)
: buffer_(buffer), pos_(0)
{
}
synced_checkup::~synced_checkup()
{
}
bool synced_checkup::local_checkup(const config& expected_data, config& real_data)
{
assert(real_data.empty());
if(buffer_.child_count("checkup") > pos_)
{
//copying objects :o
real_data = buffer_.child("checkup",pos_);
pos_ ++;
return real_data == expected_data;
}
else
{
assert(buffer_.child_count("checkup") == pos_);
buffer_.add_child("checkup", expected_data);
pos_++;
return true;
}
}
bool synced_checkup::networked_checkup(const config& /*expected_data*/, config& real_data)
{
assert(real_data.empty());
throw "not implemented";
//TODO: something with get_user_choice :).
}

81
src/synced_checkup.hpp Normal file
View file

@ -0,0 +1,81 @@
/*
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.
*/
#ifndef SYNCED_CHECKUP_H_INCLUDED
#define SYNCED_CHECKUP_H_INCLUDED
#include "config.hpp"
struct map_location;
/*
a class to check whether calculated ingame results match the results calculated during the original game.
note, that you shouldn't add new checkups to existent user actions or you might break replay compability by bringing the [checkups] tag of older saves in unorder.
so if you really want to add new checkups, you should wrap your checkup_instance->... call in a if(resources::state_of_game->classification().version ....) or similar.
*/
class checkup
{
public:
checkup();
virtual ~checkup();
/*
does only compares data in replays.
returns wether the two config objects are equal.
*/
virtual bool local_checkup(const config& expected_data, config& real_data) = 0;
/*
compares data on all clients in a networked game, the disadvantage is,
that the clients have to communicate more which might be not wanted if some persons have laggy inet.
returns whether the two config objects are equal.
this is currently not used.
*/
virtual bool networked_checkup(const config& expected_data, config& real_data) = 0;
/*
we cannot use the replay.add_checksum anymore without risks because we might send the unit_checksum after it has been recorded at the other client.
this is a helper function for that.
*/
void unit_checksum(const map_location& loc, bool local = true);
};
class synced_checkup : public checkup
{
public:
synced_checkup(config& buffer);
virtual ~synced_checkup();
virtual bool local_checkup(const config& expected_data, config& real_data);
virtual bool networked_checkup(const config& expected_data, config& real_data);
private:
config& buffer_;
unsigned int pos_;
};
/*
the only purpose of these function isto thro OOS erros, because they should never be called.
*/
class ignored_checkup : public checkup
{
public:
ignored_checkup();
virtual ~ignored_checkup();
virtual bool local_checkup(const config& expected_data, config& real_data);
virtual bool networked_checkup(const config& expected_data, config& real_data);
};
/*
this is a synced_checkup during a synced context otherwise a invalid_checkup object.
*/
extern checkup* checkup_instance;
#endif

475
src/synced_commands.cpp Normal file
View file

@ -0,0 +1,475 @@
/*
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.
*/
#include "synced_commands.hpp"
#include <cassert>
#include "log.hpp"
#include "resources.hpp"
#include "map_location.hpp"
#include "gamestatus.hpp"
#include "unit.hpp"
#include "team.hpp"
#include "play_controller.hpp"
#include "actions/create.hpp"
#include "actions/attack.hpp"
#include "actions/move.hpp"
#include "actions/undo.hpp"
#include "preferences.hpp"
#include "game_preferences.hpp"
#include "game_events/pump.hpp"
#include "dialogs.hpp"
#include "unit_helper.hpp"
#include "replay.hpp" //user choice
#include "resources.hpp"
#include <boost/foreach.hpp>
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)
/**
* @param[in] tag The replay tag for this action.
* @param[in] function The callback for this action.
*/
synced_command::synced_command(const std::string & tag, handler function)
{
assert(registry().find( tag ) == registry().end());
registry()[tag] = function;
}
synced_command::map& synced_command::registry()
{
static map* instance = new map();
return *instance;
}
SYNCED_COMMAND_HANDLER_FUNCTION(recruit, child, use_undo, show, error_handler)
{
int current_team_num = resources::controller->current_side();
team &current_team = (*resources::teams)[current_team_num - 1];
map_location loc(child, resources::gamedata);
map_location from(child.child_or_empty("from"), resources::gamedata);
// Validate "from".
if ( !from.valid() ) {
// This will be the case for AI recruits in replays saved
// before 1.11.2, so it is not more severe than a warning.
// EDIT: we borke compability with 1.11.2 anyway so we should give an error.
error_handler("Missing leader location for recruitment.\n", false);
}
else if ( resources::units->find(from) == resources::units->end() ) {
// Sync problem?
std::stringstream errbuf;
errbuf << "Recruiting leader not found at " << from << ".\n";
error_handler(errbuf.str(), false);
}
// Get the unit_type ID.
std::string type_id = child["type"];
if ( type_id.empty() ) {
error_handler("Recruitment is missing a unit type.", true);
return false;
}
const unit_type *u_type = unit_types.find(type_id);
if (!u_type) {
std::stringstream errbuf;
errbuf << "Recruiting illegal unit: '" << type_id << "'.\n";
error_handler(errbuf.str(), true);
return false;
}
const std::string res = actions::find_recruit_location(current_team_num, loc, from, type_id);
if(!res.empty())
{
std::stringstream errbuf;
errbuf << "cannot recruit unit: " << res << "\n";
error_handler(errbuf.str(), true);
return false;
//we are already oos because the unit wasn't created, no need to keep the bookkeeping right...
}
const int beginning_gold = current_team.gold();
if ( u_type->cost() > beginning_gold ) {
std::stringstream errbuf;
errbuf << "unit '" << type_id << "' is too expensive to recruit: "
<< u_type->cost() << "/" << beginning_gold << "\n";
error_handler(errbuf.str(), false);
}
actions::recruit_unit(*u_type, current_team_num, loc, from, show, use_undo, false);
LOG_REPLAY << "recruit: team=" << current_team_num << " '" << type_id << "' at (" << loc
<< ") cost=" << u_type->cost() << " from gold=" << beginning_gold << ' '
<< "-> " << current_team.gold() << "\n";
return true;
}
SYNCED_COMMAND_HANDLER_FUNCTION(recall, child, use_undo, show, error_handler)
{
int current_team_num = resources::controller->current_side();
team &current_team = (*resources::teams)[current_team_num - 1];
const std::string& unit_id = child["value"];
map_location loc(child, resources::gamedata);
map_location from(child.child_or_empty("from"), resources::gamedata);
if ( !actions::recall_unit(unit_id, current_team, loc, from, show, use_undo, false) ) {
error_handler("illegal recall: unit_id '" + unit_id + "' could not be found within the recall list.\n", true);
//when recall_unit returned false nothing happend so we can safety return false;
return false;
}
return true;
}
namespace
{
class unit_advancement_choice : public mp_sync::user_choice
{
public:
unit_advancement_choice(const map_location& loc, int total_opt, int side_num)
: loc_ (loc), nb_options_(total_opt), side_num_(side_num)
{
}
virtual ~unit_advancement_choice()
{
}
virtual config query_user() const
{
int res = 0;
team t = (*resources::teams)[side_num_ - 1];
//note, that the advancements for networked sides are also determined on the current playing side.
if(t.is_ai() || t.is_network_ai() || t.is_empty() || t.is_idle())
{
//TODO: if ai, call something like ai::choose_uni_advancement
// To make the ai_advancement_aspect work again.
res = rand() % nb_options_;
}
else if (t.is_local())
{
res = rand() % nb_options_;
assert(t.is_human());
res = dialogs::advance_unit_dialog(loc_);
}
else
{
assert(t.is_network_human());
res = 0;
}
LOG_REPLAY << "unit at position " << loc_ << "choose advancement number " << res << "\n";
config retv;
retv["value"] = res;
return retv;
}
virtual config random_choice() const
{
config retv;
retv["value"] = 0;
return retv;
}
private:
const map_location loc_;
int nb_options_;
int side_num_;
};
void advance_unit_internal(const map_location& loc)
{
//i just don't want infinite loops...
for(int advacment_number = 0; advacment_number < 20; advacment_number++)
{
unit_map::iterator u = resources::units->find(loc);
//this implies u.valid()
if(!unit_helper::will_certainly_advance(u)) {
return;
}
config selected = mp_sync::get_user_choice("choose",
unit_advancement_choice(loc, unit_helper::number_of_possible_advances(*u),u->side()));
dialogs::animate_unit_advancement(loc, selected["value"], true, true); //or pass show with the last argument?
//i want to remove the next few lines...
u = resources::units->find(loc);
// level 10 unit gives 80 XP and the highest mainline is level 5
if (u.valid() && u->experience() > 80)
{
ERR_REPLAY << "Unit has too many (" << u->experience() << ") XP left; cascade leveling disabled.\n";
return;
}
}
ERR_REPLAY << "unit at " << loc << "tried to adcance more than 21 times\n";
}
}
SYNCED_COMMAND_HANDLER_FUNCTION(attack, child, /*use_undo*/, show, error_handler)
{
const config &destination = child.child("destination");
const config &source = child.child("source");
//check_checksums(*cfg);
if (!destination || !source) {
error_handler("no destination/source found in attack\n", true);
return false;
}
//we must get locations by value instead of by references, because the iterators
//may become invalidated later
const map_location src(source, resources::gamedata);
const map_location dst(destination, resources::gamedata);
int weapon_num = child["weapon"];
int def_weapon_num = child["defender_weapon"].to_int(-2);
if (def_weapon_num == -2) {
// Let's not gratuitously destroy backwards compatibility.
WRN_REPLAY << "Old data, having to guess weapon\n";
def_weapon_num = -1;
}
unit_map::iterator u = resources::units->find(src);
if (!u.valid()) {
error_handler("unfound location for source of attack\n", true);
return false;
}
const std::string &att_type_id = child["attacker_type"];
if (u->type_id() != att_type_id) {
WRN_REPLAY << "unexpected attacker type: " << att_type_id << "(game_state gives: " << u->type_id() << ")\n";
}
if (size_t(weapon_num) >= u->attacks().size()) {
error_handler("illegal weapon type in attack\n", true);
return false;
}
unit_map::const_iterator tgt = resources::units->find(dst);
if (!tgt.valid()) {
std::stringstream errbuf;
errbuf << "unfound defender for attack: " << src << " -> " << dst << '\n';
error_handler(errbuf.str(), true);
return false;
}
const std::string &def_type_id = child["defender_type"];
if (tgt->type_id() != def_type_id) {
WRN_REPLAY << "unexpected defender type: " << def_type_id << "(game_state gives: " << tgt->type_id() << ")\n";
}
if (def_weapon_num >= static_cast<int>(tgt->attacks().size())) {
error_handler("illegal defender weapon type in attack\n", true);
return false;
}
DBG_REPLAY << "Attacker XP (before attack): " << u->experience() << "\n";
resources::undo_stack->clear();
try
{
DBG_REPLAY << "Attacking NOW!: attacker: " << src << " defender: "<< dst <<"\n";
attack_unit(src, dst, weapon_num, def_weapon_num, show);
}
catch(end_level_exception&)
{
unit_map::const_iterator atku = resources::units->find(src);
// i think this check is not needed but i'm not sure.
if (atku != resources::units->end()) {
advance_unit_internal(src);
}
unit_map::const_iterator defu = resources::units->find(dst);
if (defu != resources::units->end()) {
advance_unit_internal(dst);
}
throw;
}
unit_map::const_iterator atku = resources::units->find(src);
if (atku != resources::units->end()) {
advance_unit_internal(src);
}
unit_map::const_iterator defu = resources::units->find(dst);
if (defu != resources::units->end()) {
advance_unit_internal(dst);
}
return true;
}
SYNCED_COMMAND_HANDLER_FUNCTION(disband, child, /*use_undo*/, /*show*/, error_handler)
{
int current_team_num = resources::controller->current_side();
team &current_team = (*resources::teams)[current_team_num - 1];
const std::string& unit_id = child["value"];
std::vector<unit>::iterator disband_unit =
find_if_matches_id(current_team.recall_list(), unit_id);
if(disband_unit != current_team.recall_list().end()) {
current_team.recall_list().erase(disband_unit);
} else {
error_handler("illegal disband\n", true);
return false;
}
return true;
}
SYNCED_COMMAND_HANDLER_FUNCTION(move, child, /*use_undo*/, show, error_handler)
{
int current_team_num = resources::controller->current_side();
team &current_team = (*resources::teams)[current_team_num - 1];
const std::string& x = child["x"];
const std::string& y = child["y"];
const std::vector<map_location> steps = parse_location_range(x,y);
if(steps.empty())
{
WRN_REPLAY << "Warning: Missing path data found in [move]\n";
return false;
}
const map_location& src = steps.front();
const map_location& dst = steps.back();
if (src == dst) {
WRN_REPLAY << "Warning: Move with identical source and destination. Skipping...\n";
return false;
}
//storign the early stope
map_location early_stop(child["stop_x"].to_int(-999) - 1,
child["stop_y"].to_int(-999) - 1);
if ( !early_stop.valid() )
early_stop = dst; // Not really "early", but we need a valid stopping point.
// The nominal destination should appear to be unoccupied.
unit_map::iterator u = find_visible_unit(dst, current_team);
if ( u.valid() ) {
WRN_REPLAY << "Warning: Move destination " << dst << " appears occupied.\n";
// We'll still proceed with this movement, though, since
// an event might intervene.
// for a player it is NOT POSSIBLE to give the command to move a unit to a blocked hex,
// and it doesnt matter whether the units still stands there when the unit reaches the destination
// so this is an OOS.
}
u = resources::units->find(src);
if (!u.valid()) {
std::stringstream errbuf;
errbuf << "unfound location for source of movement: "
<< src << " -> " << dst << '\n';
error_handler(errbuf.str(), true);
return false;
}
bool show_move = show;
if ( current_team.is_ai() || current_team.is_network_ai() )
show_move = show_move && preferences::show_ai_moves();
//const int num_steps =
//todo
actions::move_unit(steps, NULL, resources::undo_stack, true,
show_move, NULL, NULL, NULL);
// Verify our destination.
/*
const map_location& actual_stop = steps[num_steps];
if ( actual_stop != early_stop ) {
std::stringstream errbuf;
errbuf << "Failed to complete movement to "
<< early_stop << ".\n";
replay::process_error(errbuf.str());
return;
}
*/
return true;
}
SYNCED_COMMAND_HANDLER_FUNCTION(fire_event, child, /*use_undo*/, /*show*/, /*error_handler*/)
{
//i don't know the reason for the following three lines.
//TODO: find out wheter we can delete them. I think this code was introduced in bbfdfcf9ed6ca44f01da32bf74c39d5fa9a75c37
BOOST_FOREACH(const config &v, child.child_range("set_variable")) {
resources::gamedata->set_variable(v["name"], v["value"]);
}
bool undoable = true;
if(const config &source = child.child("source"))
{
//the select event cannot clear he undo stack.
game_events::fire("select", map_location(source, resources::gamedata));
}
const std::string &event = child["raise"];
if (const config &source = child.child("source")) {
undoable = undoable & !game_events::fire(event, map_location(source, resources::gamedata));
} else {
undoable = undoable & !game_events::fire(event);
}
if ( !undoable)
resources::undo_stack->clear();
return true;
}
SYNCED_COMMAND_HANDLER_FUNCTION(lua_ai, child, /*use_undo*/, /*show*/, /*error_handler*/)
{
const std::string &lua_code = child["code"];
game_events::run_lua_commands(lua_code.c_str());
return true;
}
SYNCED_COMMAND_HANDLER_FUNCTION(auto_shroud, child, /*use_undo*/, /*show*/, /*error_handler*/)
{
int current_team_num = resources::controller->current_side();
team &current_team = (*resources::teams)[current_team_num - 1];
bool active = child["active"].to_bool();
// Turning on automatic shroud causes vision to be updated.
if ( active )
resources::undo_stack->commit_vision(true);
current_team.set_auto_shroud_updates(active);
return true;
}
/** from resources::undo_stack->commit_vision(bool is_replay):
* Updates fog/shroud based on the undo stack, then updates stack as needed.
* Call this when "updating shroud now".
* This may fire events and change the game state.
* @param[in] is_replay Set to true when this is called during a replay.
*
* this means it ia synced command liek any other.
*/
SYNCED_COMMAND_HANDLER_FUNCTION(update_shroud, /*child*/, /*use_undo*/, /*show*/, /*error_handler*/)
{
resources::undo_stack->commit_vision(true);
return true;
}

55
src/synced_commands.hpp Normal file
View file

@ -0,0 +1,55 @@
/*
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.
*/
#ifndef SYNCED_COMMANDS_H_INCLUDED
#define SYNCED_COMMANDS_H_INCLUDED
#include <map>
#include <exception>
#include "config.hpp"
#include <boost/function.hpp>
class synced_command {
public:
/*
the parameters or error handlers are
1) the message of the error
2) a boolean that indicates whether the error is heavy enough to make proceeding impossible.
TODO: remove the second argument because it isn't used.
*/
typedef boost::function2<void, const std::string&, bool> error_handler_function;
/*
returns: true if the action succeeded correclty,
*/
typedef bool (*handler)(const config &, bool use_undo, bool show, error_handler_function error_handler);
typedef std::map<std::string, handler> map;
synced_command(const std::string & tag, handler function);
/// using static function variable instead of static member variable to prevent static initialization fiasco when used in other files.
static map& registry();
};
/*
this is currently only used in "synced_commands.cpp" and there is no reason to use it anywhere else.
but if you have a good reason feel free to do so.
*/
#define SYNCED_COMMAND_HANDLER_FUNCTION(pname, pcfg, use_undo, show, error_handler) \
static bool synced_command_func_##pname(const config & pcfg, bool use_undo, bool show, synced_command::error_handler_function error_handler ); \
static synced_command synced_command_action_##pname(#pname, &synced_command_func_##pname); \
static bool synced_command_func_##pname(const config & pcfg, bool use_undo, bool show, synced_command::error_handler_function error_handler)
#endif

374
src/synced_context.cpp Normal file
View file

@ -0,0 +1,374 @@
#include "synced_context.hpp"
#include "synced_commands.hpp"
#include "actions/undo.hpp"
#include "ai/manager.hpp"
#include "global.hpp"
#include "config.hpp"
#include "config_assign.hpp"
#include "replay.hpp"
#include "random_new.hpp"
#include "random_new_synced.hpp"
#include "random_new_deterministic.hpp"
#include "resources.hpp"
#include "synced_checkup.hpp"
#include "gamestatus.hpp"
#include "network.hpp"
#include "log.hpp"
#include "play_controller.hpp"
#include "actions/undo.hpp"
#include "game_end_exceptions.hpp"
#include <boost/lexical_cast.hpp>
#include <cassert>
#include <stdlib.h>
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)
synced_context::syced_state synced_context::state_ = synced_context::UNSYNCED;
bool synced_context::is_simultaneously_ = false;
bool synced_context::run_in_synced_context(const std::string& commandname, const config& data, bool use_undo, bool show, bool store_in_replay, synced_command::error_handler_function error_handler)
{
DBG_REPLAY << "run_in_synced_context:" << commandname << "\n";
assert(use_undo || (!resources::undo_stack->can_redo() && !resources::undo_stack->can_undo()));
if(store_in_replay)
{
recorder.add_synced_command(commandname, data);
}
/*
use this after recorder.add_synced_command
because set_scontext_synced sets the checkup to the last added command
*/
set_scontext_synced sco(commandname);
synced_command::map::iterator it = synced_command::registry().find(commandname);
if(it == synced_command::registry().end())
{
error_handler("commandname not found", true);
}
else
{
bool success = it->second(data, use_undo, show, error_handler);
if(!success && store_in_replay)
{
//remove it from replay if we weren't sucessful.
recorder.undo();
return false;
}
}
// this might also be a good point to call resources::controller->check_victory();
// because before for example if someone kills all units during a moveto event they don't loose.
resources::controller->check_victory();
DBG_REPLAY << "run_in_synced_context end\n";
return true;
}
void synced_context::default_error_function(const std::string& message, bool /*heavy*/)
{
ERR_REPLAY << "Very strange Error during synced execution " << message;
assert(false && "Very strange Error during synced execution");
}
void synced_context::just_log_error_function(const std::string& message, bool /*heavy*/)
{
ERR_REPLAY << "Error during synced execution: " << message;
}
void synced_context::ignore_error_function(const std::string& message, bool /*heavy*/)
{
DBG_REPLAY << "Ignored during synced execution: " << message;
}
synced_context::syced_state synced_context::get_syced_state()
{
return state_;
}
void synced_context::set_syced_state(syced_state newstate)
{
state_ = newstate;
}
int synced_context::generate_random_seed()
{
random_seed_choice cho;
config retv_c = synced_context::ask_server("random_seed", cho);
return retv_c["new_seed"];
}
void synced_context::pull_remote_user_input()
{
//code copied form persist_var, feels strange to call ai::.. functions for something where the ai isn't involved....
//note that ai::manager::raise_sync_network isn't called by the ai at all anymore (one more reason to put it somehwere else)
try
{
ai::manager::raise_user_interact();
}
catch(end_turn_exception&)
{
//ignore, since it will be thwown throw again.
}
try
{
ai::manager::raise_sync_network();
}
catch(end_turn_exception&)
{
//ignore, since it will be thwown again.
}
try
{
// in some cases network::receive_data only returns the wanted result on the second try.
ai::manager::raise_sync_network();
}
catch(end_turn_exception&)
{
//ignore, since it will throw again.
}
}
boost::shared_ptr<random_new::rng> synced_context::get_rng_for(const std::string& commandname)
{
const std::string/*&*/ mode= ""; // = resources ... gamestate ... get_random_mode()
if(mode == "deterministic")
{
return boost::shared_ptr<random_new::rng>(new random_new::rng_deterministic(resources::gamedata->rng()));
}
else if (mode == "only_attacks")
{
/*
this is how is was before, it does make sense because random calculation in non attack actions
might be not important enough to pay the lag you get with asking the server for a new sed.
*/
if(commandname == "attack")
{
return boost::shared_ptr<random_new::rng>(new random_new::synced_rng(generate_random_seed));
}
else
{
return boost::shared_ptr<random_new::rng>(new random_new::rng_deterministic(resources::gamedata->rng()));
}
}
else
{
return boost::shared_ptr<random_new::rng>(new random_new::synced_rng(generate_random_seed));
}
}
config synced_context::ask_server(const std::string &name, const mp_sync::user_choice &uch)
{
assert(get_syced_state() == synced_context::SYNCED);
int current_side = resources::controller->current_side();
int side = current_side;
bool is_mp_game = network::nconnections() != 0;
const int max_side = static_cast<int>(resources::teams->size());
bool did_require = false;
if ((*resources::teams)[side-1].is_empty())
{
/*
*/
DBG_REPLAY << "MP synchronization: side 1 being null-controlled in get_user_choice.\n";
side = 1;
while ( side <= max_side && (*resources::teams)[side-1].is_empty() )
side++;
assert(side <= max_side);
}
assert(1 <= side && side <= max_side);
assert(1 <= current_side && current_side <= max_side);
DBG_REPLAY << "ask_server for :" << name << "\n";
/*
as soon as random or similar is involved, undoing is impossible.
*/
resources::undo_stack->clear();
/*
there might be speak or similar commands in the replay before the user input.
*/
while(true){
do_replay_handle(current_side, name);
// the current_side on the server is a lie because it can happen on one client we are already executing side 2
bool is_local_side = (*resources::teams)[side-1].is_local();
bool is_replay_end = get_replay_source().at_end();
if (is_replay_end && !is_mp_game)
{
/* The decision is ours, and it will be inserted
into the replay. */
DBG_REPLAY << "MP synchronization: local server choice\n";
config cfg = uch.query_user();
//-1 for "server" todo: change that.
recorder.user_input(name, cfg, -1);
return cfg;
}
else if(is_replay_end && is_mp_game)
{
DBG_REPLAY << "MP synchronization: remote server choice\n";
//here we can get into the situation that the decision has already been made but not received yet.
synced_context::pull_remote_user_input();
/*
we don't want to send multiple "require_random" to the server.
*/
if(is_local_side && !did_require)
{
config data;
data.add_child("require_random");
network::send_data(data,0);
did_require = true;
}
SDL_Delay(10);
continue;
}
else if (!is_replay_end)
{
/* The decision has already been made, and must
be extracted from the replay. */
DBG_REPLAY << "MP synchronization: replay server choice\n";
do_replay_handle(resources::controller->current_side(), name);
const config *action = get_replay_source().get_next_action();
if (!action)
{
replay::process_error("[" + name + "] expected but none found\n");
return config();
}
if (!action->has_child(name))
{
replay::process_error("[" + name + "] expected but none found, found instead:\n " + action->debug() + "\n");
return config();
}
return action->child(name);
}
}
}
set_scontext_synced::set_scontext_synced(const std::string& commandname)
: new_rng_(synced_context::get_rng_for(commandname)), new_checkup_(recorder.get_last_real_command().child_or_add("checkup"))
{
init();
}
set_scontext_synced::set_scontext_synced()
: new_rng_(synced_context::get_rng_for("")), new_checkup_(recorder.get_last_real_command().child_or_add("checkup"))
{
init();
}
set_scontext_synced::set_scontext_synced(int number)
: new_rng_(synced_context::get_rng_for("")), new_checkup_(recorder.get_last_real_command().child_or_add("checkup" + boost::lexical_cast<std::string>(number)))
{
init();
}
/*
so we dont have to write the same code 3 times.
*/
void set_scontext_synced::init()
{
LOG_REPLAY << "set_scontext_synced::set_scontext_synced\n";
assert(synced_context::get_syced_state() == synced_context::UNSYNCED);
synced_context::set_syced_state(synced_context::SYNCED);
old_checkup_ = checkup_instance;
checkup_instance = & new_checkup_;
old_rng_ = random_new::generator;
random_new::generator = new_rng_.get();
}
set_scontext_synced::~set_scontext_synced()
{
LOG_REPLAY << "set_scontext_synced:: destructor\n";
assert(synced_context::get_syced_state() == synced_context::SYNCED);
assert(checkup_instance == &new_checkup_);
config co;
if(!checkup_instance->local_checkup(config_of("random_calls", new_rng_->get_random_calls()), co))
{
//if we really get -999 we have a very serious OOS.
ERR_REPLAY << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random " << co["random_calls"].to_int(-99) << " times.\n";
ERR_REPLAY << co.debug() << "\n";
}
random_new::generator = old_rng_;
synced_context::set_syced_state(synced_context::UNSYNCED);
checkup_instance = old_checkup_;
}
int set_scontext_synced::get_random_calls()
{
return new_rng_->get_random_calls();
}
set_scontext_local_choice::set_scontext_local_choice()
{
assert(synced_context::get_syced_state() == synced_context::SYNCED);
synced_context::set_syced_state(synced_context::LOCAL_CHOICE);
old_rng_ = random_new::generator;
//calling the synced rng form inside a local_choice would cause oos.
//TODO use a member variable instead if new/delete
random_new::generator = new random_new::rng();
}
set_scontext_local_choice::~set_scontext_local_choice()
{
assert(synced_context::get_syced_state() == synced_context::LOCAL_CHOICE);
synced_context::set_syced_state(synced_context::SYNCED);
delete random_new::generator;
random_new::generator = old_rng_;
}
random_seed_choice::random_seed_choice()
{
};
random_seed_choice::~random_seed_choice()
{
};
config random_seed_choice::query_user() const
{
//getting here means we are in a sp game
config retv;
retv["new_seed"] = rand();
return retv;
};
config random_seed_choice::random_choice() const
{
//it obviously doesn't make sense to call the uninitialized random generator to generatoe a seed ofr the same random generator;
//this shoud never happen
assert(false && "random_seed_choice::random_choice called");
config retv;
retv["new_seed"] = 0;
return retv;
}

191
src/synced_context.hpp Normal file
View file

@ -0,0 +1,191 @@
/*
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.
*/
#ifndef SYNCED_CONTEXT_H_INCLUDED
#define SYNCED_CONTEXT_H_INCLUDED
#include <boost/function.hpp>
#include "synced_commands.hpp"
#include "synced_checkup.hpp"
#include "replay.hpp"
#include "random_new.hpp"
#include "random_new_synced.hpp"
#include "generic_event.hpp"
#include <boost/shared_ptr.hpp>
class config;
//only static methods.
class synced_context
{
public:
enum syced_state {UNSYNCED, SYNCED, LOCAL_CHOICE};
typedef boost::function1<void, config&> ftype;
/**
Sets the context to 'synced', initialises random context, and calls the given function.
The plan is that in replay and in real game the same function is called.
when redoing an action it does not matter whether we run them i synced context because it's save to assume that they do not have any mean effect (otherwise they couldnt be undone)
TODO: call clear undo stack in the right places (get_user_choice / ask_server) DONE
TODO: implement the "Deterministic SP Mode".
TODO: implement access to get_syced_state() in lua. DONE.
TODO: repair checkups in attacks. DONE
TODO: handle layer leaving during get_user_choice correctly DONE
TODO: move_unit currently ignores when the unit moves further than it can,
it would be good to give an oos error especially to notice cheating in mp.
TODO: movement commands are currently treated specially,
thats because actions::move_unit returns a value and some function use that value.
maybe i should add a way here to return a value.
TODO: move_unit tried to preserve the replay at all costs, the disadvantage is
1)we notice OOS later,
2)we don't notice another player cheating in MP,
3)i forgot
4)we make the code more complicated
my proposed solution would be to execute the command normally
and then just set the unit to the replay destination after we gave an OOS error.
TODO: undos are crrently recorded AFTER the action takes place,
that means you cannot disallow an action during it's execution by calling undo_stack->clear();
it would make things easier if that was possible.
TODO: the ai can currently not decide how units advance.
TODO: don't change comments in this header everytime i do something because that causes rebuild of other files ...
@param save_in_replay whether data should be saved in replay.
@param use_undo this parameter is used to ignore undos during an ai move to optimize.
@param store_in_replay only true if called by do_replay_handle
@param error_handler an error handler for the case that data contains invalid data.
@return true if the action was successful.
*/
static bool run_in_synced_context(const std::string& commandname,const config& data, bool use_undo = true, bool show = true , bool store_in_replay = true , synced_command::error_handler_function error_handler = default_error_function);
/*
Returns whether we are currently executing a synced action like recruit, start, recall, disband, movement, attack, init_side, end_turn, fire_event, lua_ai, auto_shroud or similar.
*/
static syced_state get_syced_state();
/*
should only be called form set_scontext_synced, set_scontext_local_choice
*/
static void set_syced_state(syced_state newstate);
/*
Generates a new seed for a synced event, by asking the 'server'
*/
static int generate_random_seed();
/*
called from get_user_choice;
*/
static void pull_remote_user_input();
/*
a function to be passed to run_in_synced_context to assert false on error (the default).
*/
static void default_error_function(const std::string& message, bool heavy);
/*
a function to be passed to run_in_synced_context to log the error.
*/
static void just_log_error_function(const std::string& message, bool heavy);
/*
a function to be passed to run_in_synced_context to ignore the error.
*/
static void ignore_error_function(const std::string& message, bool heavy);
/*
returns a rng_deterministic if in determinsic mode otherwise a rng_synced.
*/
static boost::shared_ptr<random_new::rng> get_rng_for(const std::string& commandname);
private:
/*
similar to get_user_choice but asks the server instead of a user.
*/
static config ask_server(const std::string &name, const mp_sync::user_choice &uch);
/*
weather we are in a synced move, in a user_choice, or none of them
*/
static syced_state state_;
/*
as soon as get_user_choice is used with side != current_side (for example in generate_random_seed) other sides execute the command simultaneously and is_simultaneously is set to true
is impossible to undo that.
also during the execution of networked turns this should always be true.
false = we are on a local turn and havent sended anything yet.
this variable is currently not used, the original plan was to use it to send data as soon as possible if is_simultaneously_= true.
*/
static bool is_simultaneously_;
/*
TODO: replace ai::manager::raise_sync_network with this event because ai::manager::raise_sync_network has nothing to do with ai anymore.
*/
static events::generic_event remote_user_input_required_;
};
/*
a RAII object to enter the synced context, cannot be called if we are already in a synced context.
*/
class set_scontext_synced
{
public:
set_scontext_synced(const std::string& commanname);
set_scontext_synced();
/*
use this if you have multiple synced_context but only one replay entry.
*/
set_scontext_synced(int num);
~set_scontext_synced();
int get_random_calls();
private:
//only called by contructors.
void init();
random_new::rng* old_rng_;
boost::shared_ptr<random_new::rng> new_rng_;
checkup* old_checkup_;
synced_checkup new_checkup_;
};
/*
a RAII object to temporary leave the synced context like in wesnoth.synchronize_choice. Can only be used from inside a synced context.
*/
class set_scontext_local_choice
{
public:
set_scontext_local_choice();
~set_scontext_local_choice();
private:
random_new::rng* old_rng_;
};
/*
a helper object to server random seed generation.
*/
class random_seed_choice : public mp_sync::user_choice
{
public:
random_seed_choice();
virtual ~random_seed_choice();
virtual config query_user() const;
virtual config random_choice() const;
};
#endif