mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-03 07:33:40 +00:00
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:
parent
da4cdef146
commit
1f794e03d8
@ -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();
|
||||
}
|
||||
|
@ -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)
|
||||
|
167
src/replay.cpp
167
src/replay.cpp
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user