use synced_context: change get_user_input

previously and afterwards there can be 2 ways an action(attack, rectuit, move...) can be executed, the first way is that the action runs on the local client, and the action is sended over the network as soon as it is completed, this happends in recruits, moves, recall, and some more. The second way is that the action is sended over the network first and is then executed on all cielents simultaniously, this happends in attack events, and similar happends in prestart/start events. This is also why mp_sync didn work in this actions before: side 2 noticed that it needs data from side 1 but side 1 did send the execeution of the action before it generated the user_input data. That's why side 2 doesnt have the user input data at this point. Previously there was one special and very bugged case when the code waited for remote input before calling get_user_choice: [get_global_variable].

This commit moves (and fixes) the "waiting" from persist_var.cpp into get_user_choice so that the caller of get_user_choice don't have to worry about it. With this we can also use get_user_choice if we already sended data over the network. And we can enable the mp_sync in attack related events and prestart events. The intention of this commit is to fix fix http://gna.org/bugs/?20871.

the plan is to sync start and prestart events, so we don't need to check for that anymore,
we also don't need to pass the rng because we can use call random_new::generator->..

this commit is part of pr 121
This commit is contained in:
gfgtdf 2014-03-24 19:37:02 +01:00
parent da4cdef146
commit 1f794e03d8
4 changed files with 137 additions and 84 deletions

View File

@ -190,7 +190,7 @@ namespace { // Types
return cfg;
}
virtual config random_choice(rand_rng::simple_rng &) const
virtual config random_choice() const
{
return config();
}
@ -221,10 +221,10 @@ namespace { // Types
return cfg;
}
virtual config random_choice(rand_rng::simple_rng &rng) const
virtual config random_choice() const
{
config cfg;
cfg["value"] = rng.get_next_random() % nb_options;
cfg["value"] = random_new::generator->next_random() % nb_options;
return cfg;
}
};
@ -1115,7 +1115,7 @@ WML_HANDLER_FUNCTION(message, event_info, cfg)
}
else
{
config choice = mp_sync::get_user_choice("input", msg, 0, true);
config choice = mp_sync::get_user_choice("input", msg);
option_chosen = choice["value"];
text_input_result = choice["text"].str();
}

View File

@ -14,7 +14,6 @@
#include "global.hpp"
#include "ai/manager.hpp"
#include "gamestatus.hpp"
#include "log.hpp"
#include "network.hpp"
@ -29,6 +28,10 @@
#include "variable.hpp"
//TODO: remove LOG_PERSIST, ERR_PERSIST from persist_context.hpp to .cpp files.
#define DBG_PERSIST LOG_STREAM(debug, log_persist)
struct persist_choice: mp_sync::user_choice {
const persist_context &ctx;
std::string var_name;
@ -44,7 +47,7 @@ struct persist_choice: mp_sync::user_choice {
ret.add_child("variables",ctx.get_var(var_name));
return ret;
}
virtual config random_choice(rand_rng::simple_rng &) const {
virtual config random_choice() const {
return config();
}
};
@ -56,7 +59,7 @@ static void get_global_variable(persist_context &ctx, const vconfig &pcfg)
config::attribute_value pcfg_side = pcfg["side"];
int side = pcfg_side.str() == "global" ? resources::controller->current_side() : pcfg_side.to_int();
persist_choice choice(ctx,global,side);
config cfg = mp_sync::get_user_choice("global_variable",choice,side,true).child("variables");
config cfg = mp_sync::get_user_choice("global_variable",choice,side).child("variables");
if (cfg) {
size_t arrsize = cfg.child_count(global);
if (arrsize == 0) {
@ -118,27 +121,17 @@ void verify_and_get_global_variable(const vconfig &pcfg)
valid = false;
}
else {
DBG_PERSIST << "verify_and_get_global_variable with from_global=" << pcfg["from_global"] << " from side " << pcfg["side"] << "\n";
config::attribute_value pcfg_side = pcfg["side"];
int side = pcfg_side.str() == "global" ? resources::controller->current_side() : pcfg_side.to_int();
if (unsigned (side - 1) >= resources::teams->size()) {
LOG_PERSIST << "Error: [get_global_variable] attribute \"side\" specifies invalid side number.";
LOG_PERSIST << "Error: [get_global_variable] attribute \"side\" specifies invalid side number." << "\n";
valid = false;
} else {
if ((side != resources::controller->current_side())
&& !((*resources::teams)[side - 1].is_local())) {
if ((*resources::teams)[resources::controller->current_side() - 1].is_local()) {
config data;
data.add_child("wait_global");
data.child("wait_global")["side"] = side;
network::send_data(data,0);
}
while (get_replay_source().at_end()) {
ai::manager::raise_user_interact();
ai::manager::raise_sync_network();
SDL_Delay(10);
}
}
}
else
{
}
DBG_PERSIST << "end verify_and_get_global_variable with from_global=" << pcfg["from_global"] << " from side " << pcfg["side"] << "\n";
}
}
if (valid)

View File

@ -21,6 +21,7 @@
#include "global.hpp"
#include "ai/manager.hpp"
#include "actions/attack.hpp"
#include "actions/create.hpp"
#include "actions/move.hpp"
@ -35,6 +36,7 @@
#include "map_label.hpp"
#include "map_location.hpp"
#include "play_controller.hpp"
#include "synced_context.hpp"
#include "replay.hpp"
#include "resources.hpp"
#include "rng.hpp"
@ -1412,73 +1414,136 @@ void replay_network_sender::commit_and_sync()
}
}
config mp_sync::get_user_choice(const std::string &name, const user_choice &uch,
int side, bool force_sp)
static config get_user_choice_internal(const std::string &name, const mp_sync::user_choice &uch, int side)
{
if (force_sp && network::nconnections() != 0 &&
resources::gamedata->phase() != game_data::PLAY)
//this should never change during the execution of this function.
int current_side = resources::controller->current_side();
if(current_side != side)
{
/* We are in a multiplayer game, during an early event which
prevents synchronization, and the WML is not interested
in a random result. We cannot silently ignore the issue,
since it would lead to a broken replay. To be sure that
the WML does not catch the error and keep the game going,
we use a sticky exception to forcefully quit. */
ERR_REPLAY << "MP synchronization does not work during prestart and start events.";
throw end_level_exception(QUIT);
//if side != current_side we send the data over the network, that means undoing is impossible
//maybe it would be better to do this in replayturn.cpp or similar. or maybe not.
resources::undo_stack->clear();
}
if (resources::gamedata->phase() == game_data::PLAY || force_sp)
/*
if we have to wait for network data, the controlling side might change due to
players leaving/ getting reassigned during raise_sync_network.
*/
while(true)
{
/* We have to communicate with the player and store the
choices in the replay. So a decision will be made on
one host and shared amongst all of them. */
/*
there might be speak or simiar comands in the replay before the user input.
*/
do_replay_handle(current_side, name);
/* process the side parameter and ensure it is within boundaries */
const int max_side = static_cast<int>(resources::teams->size());
if ( side < 1 || max_side < side )
side = resources::controller->current_side();
assert(1 <= side && side <= max_side);
// There is a chance of having a null-controlled team at this point
// (in the start event, with side 1 being null-controlled).
if ( (*resources::teams)[side-1].is_empty() )
{
// Shift the side to the first controlled side.
side = 1;
while ( side <= max_side && (*resources::teams)[side-1].is_empty() )
side++;
assert(side <= max_side);
}
if ((*resources::teams)[side-1].is_local() &&
get_replay_source().at_end())
/*
these value might change due to player left/reassign during pull_remote_user_input
*/
bool is_local_side = (*resources::teams)[side-1].is_local();
bool is_replay_end = get_replay_source().at_end();
if (is_replay_end && is_local_side)
{
set_scontext_local_choice sync;
/* The decision is ours, and it will be inserted
into the replay. */
DBG_REPLAY << "MP synchronization: local choice\n";
config cfg = uch.query_user();
recorder.user_input(name, cfg);
return cfg;
} else {
/* The decision has already been made, and must
be extracted from the replay. */
DBG_REPLAY << "MP synchronization: remote choice\n";
do_replay_handle(side, name);
}
else if(is_replay_end && !is_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";
synced_context::pull_remote_user_input();
SDL_Delay(10);
continue;
}
else if(!is_replay_end)
{
DBG_REPLAY << "MP synchronization: extracting choice from replay with is_local_side=" << is_local_side << "\n";
const config *action = get_replay_source().get_next_action();
if (!action || !*(action = &action->child(name))) {
if (!action)
{
replay::process_error("[" + name + "] expected but none found\n");
return config();
}
else if( !action->has_child(name))
{
replay::process_error("[" + name + "] expected but none found\n. found instead:\n" + action->debug());
return config();
}
return *action;
if ((*action)["side_invalid"].to_bool(false) == true)
{
WRN_REPLAY << "MP synchronization: side_invalid in replay data, this could mean someone wants to cheat.\n";
}
if ((*action)["from_side"].to_int(0) != side)
{
WRN_REPLAY << "MP synchronization: wrong from_side in replay data, this could mean someone wants to cheat.\n";
}
return action->child(name);
}
}
else
{
/* Neither the user nor a replay can be consulted, so a
decision will be made at all hosts simultaneously.
The result is not stored in the replay, since the
other clients have already taken the same decision. */
DBG_REPLAY << "MP synchronization: synchronized choice\n";
return uch.random_choice(resources::gamedata->rng());
}
}//while
}
/*
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)
{
bool is_synced = synced_context::get_syced_state() == synced_context::SYNCED;
bool is_mp_game = network::nconnections() != 0;
bool is_side_null_controlled;
const int max_side = static_cast<int>(resources::teams->size());
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 events are no synced context).\n");
return uch.query_user();
}
/*
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.\n";
}
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 likeley 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);
return get_user_choice_internal(name, uch, side);
}

View File

@ -226,7 +226,7 @@ struct user_choice
{
virtual ~user_choice() {}
virtual config query_user() const = 0;
virtual config random_choice(rand_rng::simple_rng &) const = 0;
virtual config random_choice() const = 0;
};
/**
@ -235,28 +235,23 @@ struct user_choice
* The choice is synchronized across all the multiplayer clients and
* stored into the replay. The function object is called if the local
* client is responsible for making the choice.
* otherwise this function waits for a remote choice and returns it when it is received.
* information about the choice made is saved in replay with dependent=true
*
* @param name Tag used for storing the choice into the replay.
* @param side The number of the side responsible for making the choice.
* If zero, it defaults to the currently active side.
* @param force_sp If true, user choice will happen in prestart and start
* events too. But if used for these events in multiplayer,
* an exception will be thrown instead.
*
* @note In order to prevent issues with sync, crash, or infinite loop, a
* number of precautions must be taken when getting a choice from a
* specific side.
* - The calling function must enter a loop to wait for network sync
* if the side is non-local. This loop must end when a response is
* received in the replay.
* - The server must recognize @name replay commands as legal from
* non-active players. Preferably the server should be notified
* about which player the data is expected from, and discard data
* from unexpected players.
* - do_replay_handle must ignore the @name replay command when the
* originating player's turn is reached.
*/
config get_user_choice(const std::string &name, const user_choice &uch,
int side = 0, bool force_sp = false);
int side = 0);
}