The Battle for Wesnoth  1.17.0-dev
synced_context.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2021
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "synced_context.hpp"
17 #include "synced_commands.hpp"
18 
19 #include "actions/undo.hpp"
20 #include "config.hpp"
21 #include "game_board.hpp"
22 #include "game_classification.hpp"
23 #include "game_data.hpp"
24 #include "game_end_exceptions.hpp"
25 #include "log.hpp"
27 #include "play_controller.hpp"
28 #include "random.hpp"
29 #include "random_deterministic.hpp"
30 #include "random_synced.hpp"
31 #include "replay.hpp"
32 #include "resources.hpp"
33 #include "seed_rng.hpp"
34 #include "synced_checkup.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 <iomanip>
42 #include <sstream>
43 
44 static lg::log_domain log_replay("replay");
45 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
46 #define LOG_REPLAY LOG_STREAM(info, log_replay)
47 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
48 #define ERR_REPLAY LOG_STREAM(err, log_replay)
49 
50 bool synced_context::run(const std::string& commandname,
51  const config& data,
52  bool use_undo,
53  bool show,
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
63 
65  if(it == synced_command::registry().end()) {
66  error_handler("commandname [" + commandname + "] not found");
67  } else {
68  bool success = it->second(data, use_undo, show, error_handler);
69  if(!success) {
70  return false;
71  }
72  }
73 
74  // This might also be a good point to call resources::controller->check_victory();
75  // because before for example if someone kills all units during a moveto event they don't loose.
77  sync.do_final_checkup();
78 
79  DBG_REPLAY << "run_in_synced_context end\n";
80  return true;
81 }
82 
83 bool synced_context::run_and_store(const std::string& commandname,
84  const config& data,
85  bool use_undo,
86  bool show,
88 {
89  if(resources::controller->is_replay()) {
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) {
99  }
100 
101  return success;
102 }
103 
104 bool synced_context::run_and_throw(const std::string& commandname,
105  const config& data,
106  bool use_undo,
107  bool show,
109 {
110  bool success = run_and_store(commandname, data, use_undo, show, error_handler);
111  if(success) {
113  }
114 
115  return success;
116 }
117 
118 bool synced_context::run_in_synced_context_if_not_already(const std::string& commandname,
119  const config& data,
120  bool use_undo,
121  bool show,
123 {
125  case(synced_context::UNSYNCED): {
126  return run_and_throw(commandname, data, use_undo, show, error_handler);
127  }
129  ERR_REPLAY << "trying to execute action while being in a local_choice" << std::endl;
130  // we reject it because such actions usually change the gamestate badly which is not intended during a
131  // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
132  // simultaneously so it could result in invoking different synced commands simultaneously.
133  return false;
134  case(synced_context::SYNCED): {
136  if(it == synced_command::registry().end()) {
137  error_handler("commandname [" + commandname + "] not found");
138  return false;
139  } else {
140  return it->second(data, /*use_undo*/ false, show, error_handler);
141  }
142  }
143  default:
144  assert(false && "found unknown synced_context::synced_state");
145  return false;
146  }
147 }
148 
149 void synced_context::default_error_function(const std::string& message)
150 {
151  ERR_REPLAY << "Unexpected Error during synced execution" << message << std::endl;
152  assert(!"Unexpected Error during synced execution, more info in stderr.");
153 }
154 
155 void synced_context::just_log_error_function(const std::string& message)
156 {
157  ERR_REPLAY << "Error during synced execution: " << message;
158 }
159 
160 void synced_context::ignore_error_function(const std::string& message)
161 {
162  DBG_REPLAY << "Ignored during synced execution: " << message;
163 }
164 
165 namespace
166 {
167 class random_server_choice : public synced_context::server_choice
168 {
169 public:
170  /** We are in a game with no mp server and need to do this choice locally. */
171  virtual config local_choice() const override
172  {
173  return config{"new_seed", seed_rng::next_seed_str()};
174  }
175 
176  /** The request which is sent to the mp server. */
177  virtual config request() const override
178  {
179  return config();
180  }
181 
182  virtual const char* name() const override
183  {
184  return "random_seed";
185  }
186 };
187 } // namespace
188 
190 {
191  config retv_c = synced_context::ask_server_choice(random_server_choice());
192  config::attribute_value seed_val = retv_c["new_seed"];
193 
194  return seed_val.str();
195 }
196 
198 {
200  is_simultaneous_ = true;
201 }
202 
204 {
205  // this method should only works in a synced context.
206  assert(is_synced());
207  // if we called the rng or if we sent data of this action over the network already, undoing is impossible.
209 }
210 
212 {
213  // this method only works in a synced context.
214  assert(is_synced());
216 }
217 
219 {
221 }
222 
224 {
225  assert(is_simultaneous_);
227 }
228 
229 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
230 {
231  const std::string& mode = resources::classification->random_mode;
232  if(mode == "deterministic" || mode == "biased") {
233  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
234  } else {
235  return std::make_shared<randomness::synced_rng>(generate_random_seed);
236  }
237 }
238 
240 {
242  "request_choice", config {
244  name(), request(),
245  },
246  });
247 }
248 
250 {
251  if(!is_synced()) {
252  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
253  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.\n";
254  return sch.local_choice();
255  }
256 
259  const bool is_mp_game = resources::controller->is_networked_mp();
260  bool did_require = false;
261 
262  DBG_REPLAY << "ask_server for random_seed\n";
263 
264  // As soon as random or similar is involved, undoing is impossible.
266 
267  // There might be speak or similar commands in the replay before the user input.
268  while(true) {
270  bool is_replay_end = resources::recorder->at_end();
271 
272  if(is_replay_end && !is_mp_game) {
273  // The decision is ours, and it will be inserted into the replay.
274  DBG_REPLAY << "MP synchronization: local server choice\n";
276  config cfg = sch.local_choice();
277 
278  //-1 for "server" todo: change that.
279  resources::recorder->user_input(sch.name(), cfg, -1);
280  return cfg;
281 
282  } else if(is_replay_end && is_mp_game) {
283  DBG_REPLAY << "MP synchronization: remote server choice\n";
284 
285  // Here we can get into the situation that the decision has already been made but not received yet.
287 
288  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
289  // remote choice.
291 
292  // We don't want to send multiple "require_random" to the server.
293  if(!did_require) {
294  sch.send_request();
295  did_require = true;
296  }
297 
298  SDL_Delay(10);
299  continue;
300 
301  } else if(!is_replay_end) {
302  // The decision has already been made, and must be extracted from the replay.
303  DBG_REPLAY << "MP synchronization: replay server choice\n";
305 
306  const config* action = resources::recorder->get_next_action();
307  if(!action) {
308  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
310  return sch.local_choice();
311  }
312 
313  if(!action->has_child(sch.name())) {
314  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
315  + action->debug() + "\n");
316 
318  return sch.local_choice();
319  }
320 
321  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
322  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
323  // their attack results in mp" cheat
324  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
325  }
326 
327  return action->child(sch.name());
328  }
329  }
330 }
331 
333 {
334  undo_commands_.emplace_front(commands, ctx);
335 }
336 
338  : new_rng_(synced_context::get_rng_for_action())
339  , old_rng_(randomness::generator)
340 {
341  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base\n";
342 
343  assert(!resources::whiteboard->has_planned_unit_map());
345 
348  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
350 
353 }
354 
356 {
357  LOG_REPLAY << "set_scontext_synced_base:: destructor\n";
361 }
362 
365  , new_checkup_(generate_checkup("checkup"))
366  , disabler_()
367 {
368  init();
369 }
370 
373  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
374  , disabler_()
375 {
376  init();
377 }
378 
379 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
380 {
381  if(resources::classification->oos_debug) {
382  return new mp_debug_checkup();
383  } else {
384  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
385  }
386 }
387 
388 /*
389  so we don't have to write the same code 3 times.
390 */
392 {
393  LOG_REPLAY << "set_scontext_synced::set_scontext_synced\n";
394  did_final_checkup_ = false;
397 }
398 
400 {
401  assert(!did_final_checkup_);
402  std::stringstream msg;
403  config co;
404  config cn {
405  "random_calls", new_rng_->get_random_calls(),
406  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
407  };
408 
409  if(checkup_instance->local_checkup(cn, co)) {
410  return;
411  }
412 
413  if(co["random_calls"].empty()) {
414  msg << "cannot find random_calls check in replay" << std::endl;
415  } else if(co["random_calls"] != cn["random_calls"]) {
416  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
417  << co["random_calls"].to_int() << " times." << std::endl;
418  }
419 
420  // Ignore empty next_unit_id to prevent false positives with older saves.
421  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
422  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
423  << co["next_unit_id"].to_int() << std::endl;
424  }
425 
426  if(!msg.str().empty()) {
427  msg << co.debug() << std::endl;
428  if(dont_throw) {
429  ERR_REPLAY << msg.str() << std::flush;
430  } else {
431  replay::process_error(msg.str());
432  }
433  }
434 
435  did_final_checkup_ = true;
436 }
437 
439 {
440  LOG_REPLAY << "set_scontext_synced:: destructor\n";
441  assert(checkup_instance == &*new_checkup_);
442  if(!did_final_checkup_) {
443  // do_final_checkup(true);
444  }
446 }
447 
449 {
450  return new_rng_->get_random_calls();
451 }
452 
455 {
458 
459  // calling the synced rng form inside a local_choice would cause oos.
460  // TODO: should we also reset the synced checkup?
462 }
463 
465 {
469 }
470 
472  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
473 {
474 }
static lg::log_domain log_replay("replay")
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:221
play_controller * controller
Definition: resources.cpp:22
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:402
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:42
game_classification * classification
Definition: resources.cpp:35
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:394
const randomness::mt_rng & rng() const
Definition: game_data.hpp:68
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:633
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:110
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:245
game_data * gamedata
Definition: resources.cpp:23
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:80
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:255
game_board * gameboard
Definition: resources.cpp:21
std::string next_seed_str()
Definition: seed_rng.cpp:37
#define DBG_REPLAY
checkup * checkup_instance
events::command_disabler disabler_
static bool can_undo()
replay * recorder
Definition: resources.cpp:29
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:34
static void process_error(const std::string &msg)
Definition: replay.cpp:196
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:92
#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:61
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:79
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:620
void revert_action()
Definition: replay.cpp:613
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:33
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:568
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:61
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:702
static rng & default_instance()
Definition: random.cpp:74
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
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:1347
#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:140
void increase_server_request_number()