The Battle for Wesnoth  1.15.1+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 "ai/manager.hpp"
19 #include "config.hpp"
20 #include "game_classification.hpp"
21 #include "replay.hpp"
22 #include "random.hpp"
23 #include "random_synced.hpp"
24 #include "random_deterministic.hpp"
25 #include "resources.hpp"
26 #include "synced_checkup.hpp"
27 #include "game_data.hpp"
28 #include "game_board.hpp"
29 #include "log.hpp"
31 #include "play_controller.hpp"
32 #include "actions/undo.hpp"
33 #include "game_end_exceptions.hpp"
34 #include "seed_rng.hpp"
35 #include "syncmp_handler.hpp"
36 #include "units/id.hpp"
37 #include "whiteboard/manager.hpp"
38 
39 #include <cassert>
40 #include <cstdlib>
41 #include <sstream>
42 #include <iomanip>
43 static lg::log_domain log_replay("replay");
44 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
45 #define LOG_REPLAY LOG_STREAM(info, log_replay)
46 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
47 #define ERR_REPLAY LOG_STREAM(err, log_replay)
48 
49 
54 
55 bool synced_context::run(const std::string& commandname, const config& data, bool use_undo, bool show, synced_command::error_handler_function error_handler)
56 {
57  DBG_REPLAY << "run_in_synced_context:" << commandname << "\n";
58 
59  assert(use_undo || (!resources::undo_stack->can_redo() && !resources::undo_stack->can_undo()));
60  /*
61  use this after resources::recorder->add_synced_command
62  because set_scontext_synced sets the checkup to the last added command
63  */
66  if(it == synced_command::registry().end())
67  {
68  error_handler("commandname [" +commandname +"] not found", true);
69  }
70  else
71  {
72  bool success = it->second(data, use_undo, show, error_handler);
73  if(!success)
74  {
75  return false;
76  }
77  }
78 
79  // this might also be a good point to call resources::controller->check_victory();
80  // because before for example if someone kills all units during a moveto event they don't loose.
82  sync.do_final_checkup();
83  DBG_REPLAY << "run_in_synced_context end\n";
84  return true;
85 }
86 
87 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)
88 {
89  if(resources::controller->is_replay())
90  {
91  ERR_REPLAY << "ignored attempt to invoke a synced command during replay\n";
92  return false;
93  }
94 
95  assert(resources::recorder->at_end());
96  resources::recorder->add_synced_command(commandname, data);
97  bool success = run(commandname, data, use_undo, show, error_handler);
98  if(!success)
99  {
101  }
102  return success;
103 }
104 
105 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)
106 {
107  bool success = run_and_store(commandname, data, use_undo, show, error_handler);
108  if(success)
109  {
111  }
112  return success;
113 }
114 
115 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)
116 {
118  {
120  {
121  return run_and_throw(commandname, data, use_undo, show, error_handler);
122  }
124  ERR_REPLAY << "trying to execute action while being in a local_choice" << std::endl;
125  //we reject it because such actions usually change the gamestate badly which is not intended during a local_choice.
126  //Also we cannot invoke synced commands here, because multiple clients might run local choices
127  //simultaniously so it could result in invoking different synced commands simultaniously.
128  return false;
130  {
132  if(it == synced_command::registry().end())
133  {
134  error_handler("commandname [" +commandname +"] not found", true);
135  return false;
136  }
137  else
138  {
139  return it->second(data, /*use_undo*/ false, show, error_handler);
140  }
141  }
142  default:
143  assert(false && "found unknown synced_context::synced_state");
144  return false;
145  }
146 }
147 
148 void synced_context::default_error_function(const std::string& message, bool /*heavy*/)
149 {
150  ERR_REPLAY << "Unexpected Error during synced execution" << message << std::endl;
151  assert(!"Unexpected Error during synced execution, more info in stderr.");
152 }
153 
154 void synced_context::just_log_error_function(const std::string& message, bool /*heavy*/)
155 {
156  ERR_REPLAY << "Error during synced execution: " << message;
157 }
158 
159 void synced_context::ignore_error_function(const std::string& message, bool /*heavy*/)
160 {
161  DBG_REPLAY << "Ignored during synced execution: " << message;
162 }
163 
165 {
166  return state_;
167 }
168 
170 {
171  return get_synced_state() == SYNCED;
172 }
173 
175 {
176  return get_synced_state() == UNSYNCED;
177 }
178 
180 {
181  state_ = newstate;
182 }
183 
184 namespace
185 {
186  class random_server_choice : public synced_context::server_choice
187  {
188  public:
189  /// We are in a game with no mp server and need to do this choice locally
190  virtual config local_choice() const
191  {
192  return config {"new_seed", seed_rng::next_seed_str()};
193  }
194  /// the request which is sent to the mp server.
195  virtual config request() const
196  {
197  return config();
198  }
199  virtual const char* name() const
200  {
201  return "random_seed";
202  }
203  };
204 }
206 {
207  config retv_c = synced_context::ask_server_choice(random_server_choice());
208  config::attribute_value seed_val = retv_c["new_seed"];
209 
210  return seed_val.str();
211 }
212 
214 {
215  return is_simultaneously_;
216 }
217 
219 {
220  is_simultaneously_ = false;
221 }
222 
224 {
226  is_simultaneously_ = true;
227 }
228 
230 {
231  //this method should only works in a synced context.
232  assert(is_synced());
233  //if we called the rng or if we sent data of this action over the network already, undoing is impossible.
235 }
236 
238 {
239  last_unit_id_ = id;
240 }
241 
243 {
244  //this method only works in a synced context.
245  assert(is_synced());
247 }
248 
250 {
252 }
253 
255 {
256  assert(is_simultaneously_);
258 }
259 
260 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
261 {
262  const std::string& mode = resources::classification->random_mode;
263  if(mode == "deterministic")
264  {
265  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
266  }
267  else
268  {
269  return std::make_shared<randomness::synced_rng>(generate_random_seed);
270  }
271 }
272 
274 {
276  "request_choice", config {
278  name(), request(),
279  },
280  });
281 }
282 
283 
284 
286 {
287  if (!is_synced()) {
288  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";
289  return sch.local_choice();
290  }
293  const bool is_mp_game = resources::controller->is_networked_mp();
294  bool did_require = false;
295 
296  DBG_REPLAY << "ask_server for random_seed\n";
297  /*
298  as soon as random or similar is involved, undoing is impossible.
299  */
301  /*
302  there might be speak or similar commands in the replay before the user input.
303  */
304  while(true) {
305 
307  bool is_replay_end = resources::recorder->at_end();
308 
309  if (is_replay_end && !is_mp_game)
310  {
311  /* The decision is ours, and it will be inserted
312  into the replay. */
313  DBG_REPLAY << "MP synchronization: local server choice\n";
315  config cfg = sch.local_choice();
316  //-1 for "server" todo: change that.
317  resources::recorder->user_input(sch.name(), cfg, -1);
318  return cfg;
319 
320  }
321  else if(is_replay_end && is_mp_game)
322  {
323  DBG_REPLAY << "MP synchronization: remote server choice\n";
324 
325  //here we can get into the situation that the decision has already been made but not received yet.
327  //FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a remote choice.
329  /*
330  we don't want to send multiple "require_random" to the server.
331  */
332  if(!did_require)
333  {
334  sch.send_request();
335  did_require = true;
336  }
337 
338  SDL_Delay(10);
339  continue;
340 
341  }
342  else if (!is_replay_end)
343  {
344  /* The decision has already been made, and must
345  be extracted from the replay. */
346  DBG_REPLAY << "MP synchronization: replay server choice\n";
348  const config *action = resources::recorder->get_next_action();
349  if (!action)
350  {
351  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
353  return sch.local_choice();
354  }
355  if (!action->has_child(sch.name()))
356  {
357  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n " + action->debug() + "\n");
358 
360  return sch.local_choice();
361  }
362  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false) )
363  {
364  //we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose their attack results in mp" cheat
365  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
366  }
367  return action->child(sch.name());
368  }
369  }
370 }
371 
373 {
374  undo_commands_.emplace_front(commands, ctx);
375 }
376 
378 {
379  undo_commands_.clear();
380 }
381 
383  : new_rng_(synced_context::get_rng_for_action())
384  , old_rng_(randomness::generator)
385 {
386  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base\n";
387  assert(!resources::whiteboard->has_planned_unit_map());
391  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
395 }
397 {
398  LOG_REPLAY << "set_scontext_synced_base:: destructor\n";
402 }
403 
406  , new_checkup_(generate_checkup("checkup")), disabler_()
407 {
408  init();
409 }
410 
413  , new_checkup_(generate_checkup("checkup" + std::to_string(number))), disabler_()
414 {
415  init();
416 }
417 
418 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
419 {
420  if(resources::classification->oos_debug)
421  {
422  return new mp_debug_checkup();
423  }
424  else
425  {
426  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
427  }
428 }
429 
430 /*
431  so we don't have to write the same code 3 times.
432 */
434 {
435  LOG_REPLAY << "set_scontext_synced::set_scontext_synced\n";
436  did_final_checkup_ = false;
439 }
440 
442 {
443  assert(!did_final_checkup_);
444  std::stringstream msg;
445  config co;
446  config cn {
447  "random_calls", new_rng_->get_random_calls(),
448  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
449  };
450  if(checkup_instance->local_checkup(cn, co))
451  {
452  return;
453  }
454  if(co["random_calls"].empty())
455  {
456  msg << "cannot find random_calls check in replay" << std::endl;
457  }
458  else if(co["random_calls"] != cn["random_calls"])
459  {
460  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random " << co["random_calls"].to_int() << " times." << std::endl;
461  }
462  //Ignore empty next_unit_id to prevent false positives with older saves.
463  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"])
464  {
465  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;
466  }
467  if(!msg.str().empty())
468  {
469  msg << co.debug() << std::endl;
470  if(dont_throw)
471  {
472  ERR_REPLAY << msg.str() << std::flush;
473  }
474  else
475  {
476  replay::process_error(msg.str());
477  }
478  }
479  did_final_checkup_ = true;
480 }
481 
483 {
484  LOG_REPLAY << "set_scontext_synced:: destructor\n";
485  assert(checkup_instance == &*new_checkup_);
486  if(!did_final_checkup_)
487  {
488  //do_final_checkup(true);
489  }
491 }
492 
494 {
495  return new_rng_->get_random_calls();
496 }
497 
498 
501 {
504 
505  //calling the synced rng form inside a local_choice would cause oos.
506  //TODO: should we also reset the synced checkup?
508 }
509 
511 {
515 }
516 
518  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
519 {
520 
521 }
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:626
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:243
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)
static const char * name(const std::vector< SDL_Joystick *> &joysticks, const std::size_t index)
Definition: joystick.cpp:48
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_
Managing the AIs lifecycle - headers TODO: Refactor history handling and internal commands...
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:194
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:613
static void set_synced_state(synced_state newstate)
void revert_action()
Definition: replay.cpp:606
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:561
void user_input(const std::string &, const config &, int from_side)
adds a user_input to the replay
Definition: replay.cpp:253
static void set_is_simultaneously()
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:92
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:695
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_