refactor random seed server choices

we add a more generic server choice mechnism. The plan is to use it also
for controller changes by wml.
This commit is contained in:
gfgtdf 2015-06-27 15:34:05 +02:00
parent 249aa23295
commit 3b01d92547
5 changed files with 95 additions and 42 deletions

View file

@ -102,7 +102,7 @@ game::game(player_map& players, const network::connection host,
save_replays_(save_replays),
replay_save_path_(replay_save_path),
rng_(),
last_synced_context_id_(-1) /* or maybe 0 ? it shouldn't matter*/
last_choice_request_id_(-1) /* or maybe 0 ? it shouldn't matter*/
{
assert(owner_);
players_.push_back(owner_);
@ -1009,37 +1009,26 @@ bool game::process_turn(simple_wml::document& data, const player_map::const_iter
void game::require_random(const simple_wml::document &data, const player_map::iterator user)
{
// note, that during end turn events, it's side=1 for the server but side= side_count() on the clients.
// OUTDATED INFO:
// Currently the clients make sure that one "require random seed" is sended to the server per synced context.
// This is ensured becasue only the currently active player sends the "require random seed" the drawback is,
// that in the situation that another client was faster, he has to wait for the current player to get
// to the same point where the current player then sends the "require random action".
// Then the server sends that "random_seed" to all the clients and the other client who was faster can continue.
// This can especialy happen during 'start' events where the 'current_player' is player 1.
// TODO: it would be better if all clients could send "require random seed" and then the server would just ignore
// all non-first "require random seed" per synced context. The plan could be that we add a numerical
// "synced context id" in "require random seed"
// NOTE:
// With the new strategy we allow observers to cause OOS for the playing clients by sending
// [require_random] packages based on incompatible local changes. We might want to block
// [require_random] from observers if this is a problem.
//Compability to older clients.
const simple_wml::node* require_random = data.root().child("require_random");
if(!require_random) return;
if(require_random->has_attr("request_id"))
{
int context_id = (*require_random)["request_id"].to_int();
if(context_id <= last_synced_context_id_)
if(context_id <= last_choice_request_id_)
{
// We gave already a random seed for this synced context.
return;
}
DBG_GAME << "answering seed request " << context_id << " by player " << user->second.name() << "(" << user->first << ")" << std::endl;
last_synced_context_id_ = context_id;
last_choice_request_id_ = context_id;
}
handle_random_choice(*require_random);
}
void game::handle_random_choice(const simple_wml::node&)
{
uint32_t seed = rng_.get_next_random();
std::stringstream stream;
@ -1055,8 +1044,33 @@ void game::require_random(const simple_wml::document &data, const player_map::it
send_data(*mdata, 0, "game replay");
record_data(mdata);
}
void game::handle_choice(const simple_wml::node& data, const player_map::iterator user)
{
// note, that during end turn events, it's side=1 for the server but side= side_count() on the clients.
// Otherwise we allow observers to cause OOS for the playing clients by sending
// server choice requests based on incompatible local changes. To solve this we block
// server choice requests from observers.
if (user->first != owner_ && !is_player(user->first)) {
return;
}
int request_id = lexical_cast_default<int>(data["request_id"], -10);
if(request_id <= last_choice_request_id_) {
// We gave already an anwer to this request.
return;
}
DBG_GAME << "answering seed request " << request_id << " by player " << user->second.name() << "(" << user->first << ")" << std::endl;
last_choice_request_id_ = request_id;
if(const simple_wml::node* rand = data.child("random_seed")) {
handle_random_choice(*rand);
}
}
void game::process_whiteboard(simple_wml::document& data, const player_map::const_iterator user)
{
if(!started_ || !is_player(user->first))

View file

@ -232,7 +232,11 @@ public:
void require_random(const simple_wml::document &data, const player_map::iterator user);
void reset_last_synced_context_id() { last_synced_context_id_ = -1; }
void handle_choice(const simple_wml::node& data, const player_map::iterator user);
void handle_random_choice(const simple_wml::node& data);
void reset_last_synced_context_id() { last_choice_request_id_ = -1; }
/**
* Function which returns true iff 'player' controls any of the sides spcified in 'sides'.
*/
@ -409,7 +413,7 @@ private:
/** A wrapper for mersenne twister rng which generates randomness for this game */
rand_rng::mt_rng rng_;
int last_synced_context_id_;
int last_choice_request_id_;
};
struct game_is_member {

View file

@ -2707,6 +2707,9 @@ void server::process_data_game(const network::connection sock,
} else if (data.child("require_random")) {
g.require_random(data,pl);
return;
} else if (simple_wml::node* sch = data.child("request_choice")) {
g.handle_choice(*sch, pl);
return;
} else if (data.child("message")) {
g.process_message(data, pl);
return;

View file

@ -172,9 +172,30 @@ void synced_context::set_synced_state(synced_state newstate)
state_ = newstate;
}
namespace
{
class random_server_choice : public synced_context::server_choice
{
public:
/// We are in a game with no mp server and need to do this choice locally
virtual config local_choice() const
{
return config_of("new_seed", seed_rng::next_seed_str());
}
/// the request which is sended to the mp server.
virtual config request() const
{
return config();
}
virtual const char* name() const
{
return "random_seed";
}
};
}
std::string synced_context::generate_random_seed()
{
config retv_c = synced_context::ask_server_for_seed();
config retv_c = synced_context::ask_server_choice(random_server_choice());
config::attribute_value seed_val = retv_c["new_seed"];
return seed_val.str();
@ -268,20 +289,20 @@ boost::shared_ptr<random_new::rng> synced_context::get_rng_for_action()
}
}
static void send_require_random()
void synced_context::server_choice::send_request() const
{
config data;
config& rr = data.add_child("require_random");
rr["request_id"] = resources::controller->get_server_request_number();
network::send_data(data,0);
network::send_data(config_of("request_choice", config_of
("request_id", resources::controller->get_server_request_number())
(name(), request())
));
}
config synced_context::ask_server_for_seed()
config synced_context::ask_server_choice(const server_choice& sch)
{
set_is_simultaneously();
resources::controller->increase_server_request_number();
std::string name = "random_seed";
assert(get_synced_state() == synced_context::SYNCED);
const bool is_mp_game = network::nconnections() != 0;
bool did_require = false;
@ -304,9 +325,9 @@ config synced_context::ask_server_for_seed()
/* The decision is ours, and it will be inserted
into the replay. */
DBG_REPLAY << "MP synchronization: local server choice\n";
config cfg = config_of("new_seed", seed_rng::next_seed_str());
config cfg = sch.local_choice();
//-1 for "server" todo: change that.
resources::recorder->user_input(name, cfg, -1);
resources::recorder->user_input(sch.name(), cfg, -1);
return cfg;
}
@ -322,7 +343,7 @@ config synced_context::ask_server_for_seed()
*/
if(!did_require)
{
send_require_random();
sch.send_request();
did_require = true;
}
@ -339,23 +360,23 @@ config synced_context::ask_server_for_seed()
const config *action = resources::recorder->get_next_action();
if (!action)
{
replay::process_error("[" + name + "] expected but none found\n");
replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
resources::recorder->revert_action();
return config_of("new_seed", seed_rng::next_seed_str());
return sch.local_choice();
}
if (!action->has_child(name))
if (!action->has_child(sch.name()))
{
replay::process_error("[" + name + "] expected but none found, found instead:\n " + action->debug() + "\n");
replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n " + action->debug() + "\n");
resources::recorder->revert_action();
return config_of("new_seed", seed_rng::next_seed_str());
return sch.local_choice();
}
if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false) )
{
//we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose their attack results in mp" cheat
replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
}
return action->child(name);
return action->child(sch.name());
}
}
}

View file

@ -119,11 +119,22 @@ public:
static bool can_undo();
static void set_last_unit_id(int id);
static int get_unit_id_diff();
private:
class server_choice
{
public:
/// We are in a game with no mp server and need to do this choice locally
virtual config local_choice() const = 0;
/// the request which is sended to the mp server.
virtual config request() const = 0;
virtual const char* name() const = 0;
void send_request() const;
};
/*
generates a random seed, if we are in a mp game, ask the server, otherwise generate the seed ourselves.
if we are in a mp game, ask the server, otherwise generate the answer ourselves.
*/
static config ask_server_for_seed();
static config ask_server_choice(const server_choice&);
private:
/*
weather we are in a synced move, in a user_choice, or none of them
*/