The Battle for Wesnoth  1.15.2+dev
synced_context.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2018 by David White <dave@whitevine.net>
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 #include "synced_context.hpp"
15 #include "synced_commands.hpp"
16 
17 #include "actions/undo.hpp"
18 #include "config.hpp"
19 #include "game_classification.hpp"
20 #include "replay.hpp"
21 #include "random.hpp"
22 #include "random_synced.hpp"
23 #include "random_deterministic.hpp"
24 #include "resources.hpp"
25 #include "synced_checkup.hpp"
26 #include "game_data.hpp"
27 #include "game_board.hpp"
28 #include "log.hpp"
30 #include "play_controller.hpp"
31 #include "actions/undo.hpp"
32 #include "game_end_exceptions.hpp"
33 #include "seed_rng.hpp"
34 #include "syncmp_handler.hpp"
35 #include "units/id.hpp"
36 #include "whiteboard/manager.hpp"
37 
38 #include <cassert>
39 #include <cstdlib>
40 #include <sstream>
41 #include <iomanip>
42 static lg::log_domain log_replay("replay");
43 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
44 #define LOG_REPLAY LOG_STREAM(info, log_replay)
45 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
46 #define ERR_REPLAY LOG_STREAM(err, log_replay)
47 
48 
53 
54 bool synced_context::run(const std::string& commandname, const config& data, bool use_undo, bool show, synced_command::error_handler_function error_handler)
55 {
56  DBG_REPLAY << "run_in_synced_context:" << commandname << "\n";
57 
58  assert(use_undo || (!resources::undo_stack->can_redo() && !resources::undo_stack->can_undo()));
59  /*
60  use this after resources::recorder->add_synced_command
61  because set_scontext_synced sets the checkup to the last added command
62  */
65  if(it == synced_command::registry().end())
66  {
67  error_handler("commandname [" +commandname +"] not found", true);
68  }
69  else
70  {
71  bool success = it->second(data, use_undo, show, error_handler);
72  if(!success)
73  {
74  return false;
75  }
76  }
77 
78  // this might also be a good point to call resources::controller->check_victory();
79  // because before for example if someone kills all units during a moveto event they don't loose.
81  sync.do_final_checkup();
82  DBG_REPLAY << "run_in_synced_context end\n";
83  return true;
84 }
85 
86 bool synced_context::run_and_store(const std::string& commandname, const config& data, bool use_undo, bool show, synced_command::error_handler_function error_handler)
87 {
88  if(resources::controller->is_replay())
89  {
90  ERR_REPLAY << "ignored attempt to invoke a synced command during replay\n";
91  return false;
92  }
93 
94  assert(resources::recorder->at_end());
95  resources::recorder->add_synced_command(commandname, data);
96  bool success = run(commandname, data, use_undo, show, error_handler);
97  if(!success)
98  {
100  }
101  return success;
102 }
103 
104 bool synced_context::run_and_throw(const std::string& commandname, const config& data, bool use_undo, bool show, synced_command::error_handler_function error_handler)
105 {
106  bool success = run_and_store(commandname, data, use_undo, show, error_handler);
107  if(success)
108  {
110  }
111  return success;
112 }
113 
114 bool synced_context::run_in_synced_context_if_not_already(const std::string& commandname,const config& data, bool use_undo, bool show, synced_command::error_handler_function error_handler)
115 {
117  {
119  {
120  return run_and_throw(commandname, data, use_undo, show, error_handler);
121  }
123  ERR_REPLAY << "trying to execute action while being in a local_choice" << std::endl;
124  //we reject it because such actions usually change the gamestate badly which is not intended during a local_choice.
125  //Also we cannot invoke synced commands here, because multiple clients might run local choices
126  //simultaniously so it could result in invoking different synced commands simultaniously.
127  return false;
129  {
131  if(it == synced_command::registry().end())
132  {
133  error_handler("commandname [" +commandname +"] not found", true);
134  return false;
135  }
136  else
137  {
138  return it->second(data, /*use_undo*/ false, show, error_handler);
139  }
140  }
141  default:
142  assert(false && "found unknown synced_context::synced_state");
143  return false;
144  }
145 }
146 
147 void synced_context::default_error_function(const std::string& message, bool /*heavy*/)
148 {
149  ERR_REPLAY << "Unexpected Error during synced execution" << message << std::endl;
150  assert(!"Unexpected Error during synced execution, more info in stderr.");
151 }
152 
153 void synced_context::just_log_error_function(const std::string& message, bool /*heavy*/)
154 {
155  ERR_REPLAY << "Error during synced execution: " << message;
156 }
157 
158 void synced_context::ignore_error_function(const std::string& message, bool /*heavy*/)
159 {
160  DBG_REPLAY << "Ignored during synced execution: " << message;
161 }
162 
164 {
165  return state_;
166 }
167 
169 {
170  return get_synced_state() == SYNCED;
171 }
172 
174 {
175  return get_synced_state() == UNSYNCED;
176 }
177 
179 {
180  state_ = newstate;
181 }
182 
183 namespace
184 {
185  class random_server_choice : public synced_context::server_choice
186  {
187  public:
188  /// We are in a game with no mp server and need to do this choice locally
189  virtual config local_choice() const
190  {
191  return config {"new_seed", seed_rng::next_seed_str()};
192  }
193  /// the request which is sent to the mp server.
194  virtual config request() const
195  {
196  return config();
197  }
198  virtual const char* name() const
199  {
200  return "random_seed";
201  }
202  };
203 }
205 {
206  config retv_c = synced_context::ask_server_choice(random_server_choice());
207  config::attribute_value seed_val = retv_c["new_seed"];
208 
209  return seed_val.str();
210 }
211 
213 {
214  return is_simultaneously_;
215 }
216 
218 {
219  is_simultaneously_ = false;
220 }
221 
223 {
225  is_simultaneously_ = true;
226 }
227 
229 {
230  //this method should only works in a synced context.
231  assert(is_synced());
232  //if we called the rng or if we sent data of this action over the network already, undoing is impossible.
234 }
235 
237 {
238  last_unit_id_ = id;
239 }
240 
242 {
243  //this method only works in a synced context.
244  assert(is_synced());
246 }
247 
249 {
251 }
252 
254 {
255  assert(is_simultaneously_);
257 }
258 
259 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
260 {
261  const std::string& mode = resources::classification->random_mode;
262  if(mode == "deterministic")
263  {
264  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
265  }
266  else
267  {
268  return std::make_shared<randomness::synced_rng>(generate_random_seed);
269  }
270 }
271 
273 {
275  "request_choice", config {
277  name(), request(),
278  },
279  });
280 }
281 
282 
283 
285 {
286  if (!is_synced()) {
287  ERR_REPLAY << "Trying to ask the server for a '" << sch.name() << "' choice in a unsynced context, doing the choice locally. This can cause OOS.\n";
288  return sch.local_choice();
289  }
292  const bool is_mp_game = resources::controller->is_networked_mp();
293  bool did_require = false;
294 
295  DBG_REPLAY << "ask_server for random_seed\n";
296  /*
297  as soon as random or similar is involved, undoing is impossible.
298  */
300  /*
301  there might be speak or similar commands in the replay before the user input.
302  */
303  while(true) {
304 
306  bool is_replay_end = resources::recorder->at_end();
307 
308  if (is_replay_end && !is_mp_game)
309  {
310  /* The decision is ours, and it will be inserted
311  into the replay. */
312  DBG_REPLAY << "MP synchronization: local server choice\n";
314  config cfg = sch.local_choice();
315  //-1 for "server" todo: change that.
316  resources::recorder->user_input(sch.name(), cfg, -1);
317  return cfg;
318 
319  }
320  else if(is_replay_end && is_mp_game)
321  {
322  DBG_REPLAY << "MP synchronization: remote server choice\n";
323 
324  //here we can get into the situation that the decision has already been made but not received yet.
326  //FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a remote choice.
328  /*
329  we don't want to send multiple "require_random" to the server.
330  */
331  if(!did_require)
332  {
333  sch.send_request();
334  did_require = true;
335  }
336 
337  SDL_Delay(10);
338  continue;
339 
340  }
341  else if (!is_replay_end)
342  {
343  /* The decision has already been made, and must
344  be extracted from the replay. */
345  DBG_REPLAY << "MP synchronization: replay server choice\n";
347  const config *action = resources::recorder->get_next_action();
348  if (!action)
349  {
350  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
352  return sch.local_choice();
353  }
354  if (!action->has_child(sch.name()))
355  {
356  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n " + action->debug() + "\n");
357 
359  return sch.local_choice();
360  }
361  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false) )
362  {
363  //we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose their attack results in mp" cheat
364  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
365  }
366  return action->child(sch.name());
367  }
368  }
369 }
370 
372 {
373  undo_commands_.emplace_front(commands, ctx);
374 }
375 
377 {
378  undo_commands_.clear();
379 }
380 
382  : new_rng_(synced_context::get_rng_for_action())
383  , old_rng_(randomness::generator)
384 {
385  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base\n";
386  assert(!resources::whiteboard->has_planned_unit_map());
390  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
394 }
396 {
397  LOG_REPLAY << "set_scontext_synced_base:: destructor\n";
401 }
402 
405  , new_checkup_(generate_checkup("checkup")), disabler_()
406 {
407  init();
408 }
409 
412  , new_checkup_(generate_checkup("checkup" + std::to_string(number))), disabler_()
413 {
414  init();
415 }
416 
417 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
418 {
419  if(resources::classification->oos_debug)
420  {
421  return new mp_debug_checkup();
422  }
423  else
424  {
425  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
426  }
427 }
428 
429 /*
430  so we don't have to write the same code 3 times.
431 */
433 {
434  LOG_REPLAY << "set_scontext_synced::set_scontext_synced\n";
435  did_final_checkup_ = false;
438 }
439 
441 {
442  assert(!did_final_checkup_);
443  std::stringstream msg;
444  config co;
445  config cn {
446  "random_calls", new_rng_->get_random_calls(),
447  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
448  };
449  if(checkup_instance->local_checkup(cn, co))
450  {
451  return;
452  }
453  if(co["random_calls"].empty())
454  {
455  msg << "cannot find random_calls check in replay" << std::endl;
456  }
457  else if(co["random_calls"] != cn["random_calls"])
458  {
459  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random " << co["random_calls"].to_int() << " times." << std::endl;
460  }
461  //Ignore empty next_unit_id to prevent false positives with older saves.
462  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"])
463  {
464  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was " << co["next_unit_id"].to_int() << std::endl;
465  }
466  if(!msg.str().empty())
467  {
468  msg << co.debug() << std::endl;
469  if(dont_throw)
470  {
471  ERR_REPLAY << msg.str() << std::flush;
472  }
473  else
474  {
475  replay::process_error(msg.str());
476  }
477  }
478  did_final_checkup_ = true;
479 }
480 
482 {
483  LOG_REPLAY << "set_scontext_synced:: destructor\n";
484  assert(checkup_instance == &*new_checkup_);
485  if(!did_final_checkup_)
486  {
487  //do_final_checkup(true);
488  }
490 }
491 
493 {
494  return new_rng_->get_random_calls();
495 }
496 
497 
500 {
503 
504  //calling the synced rng form inside a local_choice would cause oos.
505  //TODO: should we also reset the synced checkup?
507 }
508 
510 {
514 }
515 
517  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
518 {
519 
520 }
static lg::log_domain log_replay("replay")
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:220
play_controller * controller
Definition: resources.cpp:21
static void add_undo_commands(const config &commands, const game_events::queued_event &ctx)
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Definition: config.cpp:420
static bool run_and_store(const std::string &commandname, const config &data, bool use_undo=true, bool show=true, synced_command::error_handler_function error_handler=default_error_function)
std::size_t get_save_id() const
Used for saving id to savegame.
Definition: id.cpp:41
game_classification * classification
Definition: resources.cpp:34
void do_final_checkup(bool dont_throw=false)
Variant for storing WML attributes.
static bool run(const std::string &commandname, const config &data, bool use_undo=true, bool show=true, synced_command::error_handler_function error_handler=default_error_function)
Sets the context to &#39;synced&#39;, initialises random context, and calls the given function.
static map & registry()
using static function variable instead of static member variable to prevent static initialization fia...
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:412
const randomness::mt_rng & rng() const
Definition: game_data.hpp:65
static std::string generate_random_seed()
static bool is_unsynced()
bool at_end() const
Definition: replay.cpp:627
virtual config local_choice() const =0
We are in a game with no mp server and need to do this choice locally.
randomness::rng * old_rng_
virtual void play_slice(bool is_delay_enabled=true)
static void pull_remote_choice()
STL namespace.
Replay control code.
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
void check_victory()
Checks to see if a side has won.
Contains the exception interfaces used to signal completion of a scenario, campaign or turn...
Definitions for the interface to Wesnoth Markup Language (WML).
void add_synced_command(const std::string &name, const config &command)
Definition: replay.cpp:244
static synced_state get_synced_state()
static event_list undo_commands_
Actions wml to be executed when the current action is undone.
game_data * gamedata
Definition: resources.cpp:22
static void reset_is_simultaneously()
int get_server_request_number() const
static void set_last_unit_id(int id)
std::shared_ptr< randomness::rng > new_rng_
unsigned int get_random_calls() const
Provides the number of random calls to the rng in this context.
Definition: random.cpp:79
static void just_log_error_function(const std::string &message, bool heavy)
a function to be passed to run_in_synced_context to log the error.
static synced_state state_
virtual const char * name() const =0
static void ignore_error_function(const std::string &message, bool heavy)
a function to be passed to run_in_synced_context to ignore the error.
game_board * gameboard
Definition: resources.cpp:20
std::string next_seed_str()
Definition: seed_rng.cpp:54
#define DBG_REPLAY
checkup * checkup_instance
events::command_disabler disabler_
static bool can_undo()
std::deque< std::pair< config, game_events::queued_event > > event_list
replay * recorder
Definition: resources.cpp:28
This checkup compares whether the results calculated during the original game match the ones calculat...
static void send_user_choice()
called from get_user_choice to send a recently made choice to the other clients.
static void pull_remote_user_input()
called from get_user_choice while waiting for a remove user choice.
static int last_unit_id_
Used to restore the unit id manager when undoing.
static bool is_simultaneously()
std::shared_ptr< wb::manager > whiteboard
Definition: resources.cpp:33
static void process_error(const std::string &msg)
Definition: replay.cpp:195
static checkup * generate_checkup(const std::string &tagname)
A class to check whether the results that were calculated in the replay match the results calculated ...
virtual bool is_networked_mp() const
static int get_unit_id_diff()
randomness::rng * old_rng_
static std::string flush(std::ostringstream &s)
Definition: reports.cpp:90
#define LOG_REPLAY
static void send_user_choice()
static void reset_undo_commands()
rng * generator
This generator is automatically synced during synced context.
Definition: random.cpp:60
const std::unique_ptr< checkup > new_checkup_
static bool run_and_throw(const std::string &commandname, const config &data, bool use_undo=true, bool show=true, synced_command::error_handler_function error_handler=default_error_function)
n_unit::id_manager & unit_id_manager()
Definition: game_board.hpp:86
static bool run_in_synced_context_if_not_already(const std::string &commandname, const config &data, bool use_undo=true, bool show=true, synced_command::error_handler_function error_handler=default_error_function)
checks whether we are currently running in a synced context, and if not we enters it...
void maybe_throw_return_to_play_side() const
Various functions that implement the undoing (and redoing) of in-game commands.
Standard logging facilities (interface).
config * get_next_action()
Definition: replay.cpp:614
static void set_synced_state(synced_state newstate)
void revert_action()
Definition: replay.cpp:607
static void default_error_function(const std::string &message, bool heavy)
a function to be passed to run_in_synced_context to assert false on error (the default).
static std::shared_ptr< randomness::rng > get_rng_for_action()
std::function< void(const std::string &, bool)> error_handler_function
actions::undo_list * undo_stack
Definition: resources.cpp:32
virtual bool local_checkup(const config &expected_data, config &real_data)=0
Compares data to the results calculated during the original game.
void undo()
Definition: replay.cpp:562
void user_input(const std::string &, const config &, int from_side)
adds a user_input to the replay
Definition: replay.cpp:254
static void set_is_simultaneously()
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:696
static rng & default_instance()
Definition: random.cpp:73
static bool is_synced()
std::string::const_iterator iterator
Definition: tokenizer.hpp:24
This checkup always compares the results in from different clients in a mp game but it also causes mo...
virtual void send_to_wesnothd(const config &, const std::string &="unknown") const
std::string debug() const
Definition: config.cpp:1277
#define ERR_REPLAY
static config ask_server_choice(const server_choice &)
std::string str(const std::string &fallback="") const
void show(const std::string &window_id, const t_string &message, const point &mouse, const SDL_Rect &source_rect)
Shows a tip.
Definition: tooltip.cpp:154
void increase_server_request_number()
static bool is_simultaneously_