The Battle for Wesnoth  1.15.13+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 
15 #include "synced_context.hpp"
16 #include "synced_commands.hpp"
17 
18 #include "actions/undo.hpp"
19 #include "config.hpp"
20 #include "game_board.hpp"
21 #include "game_classification.hpp"
22 #include "game_data.hpp"
23 #include "game_end_exceptions.hpp"
24 #include "log.hpp"
26 #include "play_controller.hpp"
27 #include "random.hpp"
28 #include "random_deterministic.hpp"
29 #include "random_synced.hpp"
30 #include "replay.hpp"
31 #include "resources.hpp"
32 #include "seed_rng.hpp"
33 #include "synced_checkup.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 <iomanip>
41 #include <sstream>
42 
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 bool synced_context::run(const std::string& commandname,
50  const config& data,
51  bool use_undo,
52  bool show,
54 {
55  DBG_REPLAY << "run_in_synced_context:" << commandname << "\n";
56 
57  assert(use_undo || (!resources::undo_stack->can_redo() && !resources::undo_stack->can_undo()));
58 
59  // use this after resources::recorder->add_synced_command
60  // because set_scontext_synced sets the checkup to the last added command
62 
64  if(it == synced_command::registry().end()) {
65  error_handler("commandname [" + commandname + "] not found");
66  } else {
67  bool success = it->second(data, use_undo, show, error_handler);
68  if(!success) {
69  return false;
70  }
71  }
72 
73  // This might also be a good point to call resources::controller->check_victory();
74  // because before for example if someone kills all units during a moveto event they don't loose.
76  sync.do_final_checkup();
77 
78  DBG_REPLAY << "run_in_synced_context end\n";
79  return true;
80 }
81 
82 bool synced_context::run_and_store(const std::string& commandname,
83  const config& data,
84  bool use_undo,
85  bool show,
87 {
88  if(resources::controller->is_replay()) {
89  ERR_REPLAY << "ignored attempt to invoke a synced command during replay\n";
90  return false;
91  }
92 
93  assert(resources::recorder->at_end());
94  resources::recorder->add_synced_command(commandname, data);
95  bool success = run(commandname, data, use_undo, show, error_handler);
96  if(!success) {
98  }
99 
100  return success;
101 }
102 
103 bool synced_context::run_and_throw(const std::string& commandname,
104  const config& data,
105  bool use_undo,
106  bool show,
108 {
109  bool success = run_and_store(commandname, data, use_undo, show, error_handler);
110  if(success) {
112  }
113 
114  return success;
115 }
116 
117 bool synced_context::run_in_synced_context_if_not_already(const std::string& commandname,
118  const config& data,
119  bool use_undo,
120  bool show,
122 {
124  case(synced_context::UNSYNCED): {
125  return run_and_throw(commandname, data, use_undo, show, error_handler);
126  }
128  ERR_REPLAY << "trying to execute action while being in a local_choice" << std::endl;
129  // we reject it because such actions usually change the gamestate badly which is not intended during a
130  // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
131  // simultaneously so it could result in invoking different synced commands simultaneously.
132  return false;
133  case(synced_context::SYNCED): {
135  if(it == synced_command::registry().end()) {
136  error_handler("commandname [" + commandname + "] not found");
137  return false;
138  } else {
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)
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)
155 {
156  ERR_REPLAY << "Error during synced execution: " << message;
157 }
158 
159 void synced_context::ignore_error_function(const std::string& message)
160 {
161  DBG_REPLAY << "Ignored during synced execution: " << message;
162 }
163 
164 namespace
165 {
166 class random_server_choice : public synced_context::server_choice
167 {
168 public:
169  /** We are in a game with no mp server and need to do this choice locally. */
170  virtual config local_choice() const override
171  {
172  return config{"new_seed", seed_rng::next_seed_str()};
173  }
174 
175  /** The request which is sent to the mp server. */
176  virtual config request() const override
177  {
178  return config();
179  }
180 
181  virtual const char* name() const override
182  {
183  return "random_seed";
184  }
185 };
186 } // namespace
187 
189 {
190  config retv_c = synced_context::ask_server_choice(random_server_choice());
191  config::attribute_value seed_val = retv_c["new_seed"];
192 
193  return seed_val.str();
194 }
195 
197 {
199  is_simultaneous_ = true;
200 }
201 
203 {
204  // this method should only works in a synced context.
205  assert(is_synced());
206  // if we called the rng or if we sent data of this action over the network already, undoing is impossible.
208 }
209 
211 {
212  // this method only works in a synced context.
213  assert(is_synced());
215 }
216 
218 {
220 }
221 
223 {
224  assert(is_simultaneous_);
226 }
227 
228 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
229 {
230  const std::string& mode = resources::classification->random_mode;
231  if(mode == "deterministic" || mode == "biased") {
232  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
233  } else {
234  return std::make_shared<randomness::synced_rng>(generate_random_seed);
235  }
236 }
237 
239 {
241  "request_choice", config {
243  name(), request(),
244  },
245  });
246 }
247 
249 {
250  if(!is_synced()) {
251  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
252  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.\n";
253  return sch.local_choice();
254  }
255 
258  const bool is_mp_game = resources::controller->is_networked_mp();
259  bool did_require = false;
260 
261  DBG_REPLAY << "ask_server for random_seed\n";
262 
263  // As soon as random or similar is involved, undoing is impossible.
265 
266  // There might be speak or similar commands in the replay before the user input.
267  while(true) {
269  bool is_replay_end = resources::recorder->at_end();
270 
271  if(is_replay_end && !is_mp_game) {
272  // The decision is ours, and it will be inserted into the replay.
273  DBG_REPLAY << "MP synchronization: local server choice\n";
275  config cfg = sch.local_choice();
276 
277  //-1 for "server" todo: change that.
278  resources::recorder->user_input(sch.name(), cfg, -1);
279  return cfg;
280 
281  } else if(is_replay_end && is_mp_game) {
282  DBG_REPLAY << "MP synchronization: remote server choice\n";
283 
284  // Here we can get into the situation that the decision has already been made but not received yet.
286 
287  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
288  // remote choice.
290 
291  // We don't want to send multiple "require_random" to the server.
292  if(!did_require) {
293  sch.send_request();
294  did_require = true;
295  }
296 
297  SDL_Delay(10);
298  continue;
299 
300  } else if(!is_replay_end) {
301  // The decision has already been made, and must be extracted from the replay.
302  DBG_REPLAY << "MP synchronization: replay server choice\n";
304 
305  const config* action = resources::recorder->get_next_action();
306  if(!action) {
307  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
309  return sch.local_choice();
310  }
311 
312  if(!action->has_child(sch.name())) {
313  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
314  + action->debug() + "\n");
315 
317  return sch.local_choice();
318  }
319 
320  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
321  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
322  // their attack results in mp" cheat
323  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
324  }
325 
326  return action->child(sch.name());
327  }
328  }
329 }
330 
332 {
333  undo_commands_.emplace_front(commands, ctx);
334 }
335 
337  : new_rng_(synced_context::get_rng_for_action())
338  , old_rng_(randomness::generator)
339 {
340  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base\n";
341 
342  assert(!resources::whiteboard->has_planned_unit_map());
344 
347  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
349 
352 }
353 
355 {
356  LOG_REPLAY << "set_scontext_synced_base:: destructor\n";
360 }
361 
364  , new_checkup_(generate_checkup("checkup"))
365  , disabler_()
366 {
367  init();
368 }
369 
372  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
373  , disabler_()
374 {
375  init();
376 }
377 
378 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
379 {
380  if(resources::classification->oos_debug) {
381  return new mp_debug_checkup();
382  } else {
383  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
384  }
385 }
386 
387 /*
388  so we don't have to write the same code 3 times.
389 */
391 {
392  LOG_REPLAY << "set_scontext_synced::set_scontext_synced\n";
393  did_final_checkup_ = false;
396 }
397 
399 {
400  assert(!did_final_checkup_);
401  std::stringstream msg;
402  config co;
403  config cn {
404  "random_calls", new_rng_->get_random_calls(),
405  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
406  };
407 
408  if(checkup_instance->local_checkup(cn, co)) {
409  return;
410  }
411 
412  if(co["random_calls"].empty()) {
413  msg << "cannot find random_calls check in replay" << std::endl;
414  } else if(co["random_calls"] != cn["random_calls"]) {
415  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
416  << co["random_calls"].to_int() << " times." << std::endl;
417  }
418 
419  // Ignore empty next_unit_id to prevent false positives with older saves.
420  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
421  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
422  << co["next_unit_id"].to_int() << std::endl;
423  }
424 
425  if(!msg.str().empty()) {
426  msg << co.debug() << std::endl;
427  if(dont_throw) {
428  ERR_REPLAY << msg.str() << std::flush;
429  } else {
430  replay::process_error(msg.str());
431  }
432  }
433 
434  did_final_checkup_ = true;
435 }
436 
438 {
439  LOG_REPLAY << "set_scontext_synced:: destructor\n";
440  assert(checkup_instance == &*new_checkup_);
441  if(!did_final_checkup_) {
442  // do_final_checkup(true);
443  }
445 }
446 
448 {
449  return new_rng_->get_random_calls();
450 }
451 
454 {
457 
458  // calling the synced rng form inside a local_choice would cause oos.
459  // TODO: should we also reset the synced checkup?
461 }
462 
464 {
468 }
469 
471  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
472 {
473 }
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)
static synced_state get_synced_state()
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:414
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 void set_synced_state(synced_state newstate)
Should only be called form set_scontext_synced, set_scontext_local_choice.
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:406
const randomness::mt_rng & rng() const
Definition: game_data.hpp:67
static std::string generate_random_seed()
Generates a new seed for a synced event, by asking the &#39;server&#39;.
bool at_end() const
Definition: replay.cpp:629
static void set_is_simultaneous()
Sets is_simultaneous_ = true, called using a user choice that is not the currently playing side...
static event_list undo_commands_
Actions wml to be executed when the current action is undone.
static bool is_simultaneous_
As soon as get_user_choice is used with side != current_side (for example in generate_random_seed) ot...
virtual config local_choice() const =0
We are in a game with no mp server and need to do this choice locally.
A RAII object to enter the synced context, cannot be called if we are already in a synced context...
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
game_data * gamedata
Definition: resources.cpp:22
int get_server_request_number() const
static void just_log_error_function(const std::string &message)
A function to be passed to run_in_synced_context to log the error.
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
virtual const char * name() const =0
static void reset_undo_commands()
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition: replay.cpp:254
game_board * gameboard
Definition: resources.cpp:20
std::string next_seed_str()
Definition: seed_rng.cpp:36
#define DBG_REPLAY
checkup * checkup_instance
events::command_disabler disabler_
static bool can_undo()
replay * recorder
Definition: resources.cpp:28
This checkup compares whether the results calculated during the original game match the ones calculat...
std::function< void(const std::string &)> error_handler_function
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_synced()
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:91
#define LOG_REPLAY
static void send_user_choice()
static void set_last_unit_id(int id)
static void ignore_error_function(const std::string &message)
A function to be passed to run_in_synced_context to ignore the error.
rng * generator
This generator is automatically synced during synced context.
Definition: random.cpp:60
static void default_error_function(const std::string &message)
A function to be passed to run_in_synced_context to assert false on error (the default).
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:78
static void reset_is_simultaneous()
Sets is_simultaneous_ = false, called when entering the synced context.
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:616
void revert_action()
Definition: replay.cpp:609
static std::shared_ptr< randomness::rng > get_rng_for_action()
A RAII object to temporary leave the synced context like in wesnoth.synchronize_choice.
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:564
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:59
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:698
static rng & default_instance()
Definition: random.cpp:73
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:1322
#define ERR_REPLAY
static config ask_server_choice(const server_choice &)
If we are in a mp game, ask the server, otherwise generate the answer ourselves.
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:139
void increase_server_request_number()