The Battle for Wesnoth  1.17.23+dev
synced_context.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2023
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;
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 
75 
76  sync.do_final_checkup();
77 
78  // TODO: It would be nice if this could automaticially detect that
79  // no entry was pushed to the undo stack for this action
80  // and always clear the undo stack in that case.
81  if(undo_blocked()) {
82  // This in particular helps the networking code to make sure this command is sent.
85  }
86 
87  DBG_REPLAY << "run_in_synced_context end";
88  return true;
89 }
90 
91 bool synced_context::run_and_store(const std::string& commandname,
92  const config& data,
93  bool use_undo,
94  bool show,
96 {
97  if(resources::controller->is_replay()) {
98  ERR_REPLAY << "ignored attempt to invoke a synced command during replay";
99  return false;
100  }
101 
102  assert(resources::recorder->at_end());
104  bool success = run(commandname, data, use_undo, show, error_handler);
105  if(!success) {
107  }
108 
109  return success;
110 }
111 
112 bool synced_context::run_and_throw(const std::string& commandname,
113  const config& data,
114  bool use_undo,
115  bool show,
117 {
118  bool success = run_and_store(commandname, data, use_undo, show, error_handler);
119  if(success) {
121  }
122 
123  return success;
124 }
125 
126 bool synced_context::run_in_synced_context_if_not_already(const std::string& commandname,
127  const config& data,
128  bool use_undo,
129  bool show,
131 {
133  case(synced_context::UNSYNCED): {
134  return run_and_throw(commandname, data, use_undo, show, error_handler);
135  }
137  ERR_REPLAY << "trying to execute action while being in a local_choice";
138  // we reject it because such actions usually change the gamestate badly which is not intended during a
139  // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
140  // simultaneously so it could result in invoking different synced commands simultaneously.
141  return false;
142  case(synced_context::SYNCED): {
144  if(it == synced_command::registry().end()) {
145  error_handler("commandname [" + commandname + "] not found");
146  return false;
147  } else {
148  return it->second(data, /*use_undo*/ false, show, error_handler);
149  }
150  }
151  default:
152  assert(false && "found unknown synced_context::synced_state");
153  return false;
154  }
155 }
156 
157 void synced_context::default_error_function(const std::string& message)
158 {
159  ERR_REPLAY << "Unexpected Error during synced execution" << message;
160  assert(!"Unexpected Error during synced execution, more info in stderr.");
161 }
162 
163 void synced_context::just_log_error_function(const std::string& message)
164 {
165  ERR_REPLAY << "Error during synced execution: " << message;
166 }
167 
168 void synced_context::ignore_error_function(const std::string& message)
169 {
170  DBG_REPLAY << "Ignored during synced execution: " << message;
171 }
172 
173 namespace
174 {
175 class random_server_choice : public synced_context::server_choice
176 {
177 public:
178  /** We are in a game with no mp server and need to do this choice locally. */
179  virtual config local_choice() const override
180  {
181  return config{"new_seed", seed_rng::next_seed_str()};
182  }
183 
184  /** The request which is sent to the mp server. */
185  virtual config request() const override
186  {
187  return config();
188  }
189 
190  virtual const char* name() const override
191  {
192  return "random_seed";
193  }
194 };
195 } // namespace
196 
198 {
199  config retv_c = synced_context::ask_server_choice(random_server_choice());
200  config::attribute_value seed_val = retv_c["new_seed"];
201 
202  return seed_val.str();
203 }
204 
206 {
208  is_simultaneous_ = true;
209 }
210 
212 {
213  // this method should only works in a synced context.
214  assert(!is_unsynced());
215  // if we sent data of this action over the network already, undoing is blocked.
216  // if we called the rng, undoing is blocked.
217  // if the game has ended, undoing is blocked.
218  // if the turn has ended undoing is blocked.
219  return is_simultaneous_
221  || resources::controller->is_regular_game_end()
223 }
224 
226 {
227  // this method only works in a synced context.
228  assert(is_synced());
230 }
231 
233 {
235 }
236 
237 // TODO: this is now also used for normal actions, maybe it should be renamed.
239 {
240  assert(undo_blocked());
242 }
243 
244 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
245 {
246  const std::string& mode = resources::classification->random_mode;
247  if(mode == "deterministic" || mode == "biased") {
248  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
249  } else {
250  return std::make_shared<randomness::synced_rng>(generate_random_seed);
251  }
252 }
253 
255 {
257 }
258 
260 {
262  "request_choice", config {
263  "request_id", request_id(),
264  name(), request(),
265  },
266  });
267 }
268 
270 {
271  if(!is_synced()) {
272  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
273  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.";
274  return sch.local_choice();
275  }
276 
279  const bool is_mp_game = resources::controller->is_networked_mp();
280  bool did_require = false;
281 
282  DBG_REPLAY << "ask_server for random_seed";
283 
284  // As soon as random or similar is involved, undoing is impossible.
286 
287  // There might be speak or similar commands in the replay before the user input.
288  while(true) {
290  bool is_replay_end = resources::recorder->at_end();
291 
292  if(is_replay_end && !is_mp_game) {
293  // The decision is ours, and it will be inserted into the replay.
294  DBG_REPLAY << "MP synchronization: local server choice";
296  config cfg = sch.local_choice();
297  cfg["request_id"] = sch.request_id();
298  //-1 for "server" todo: change that.
299  resources::recorder->user_input(sch.name(), cfg, -1);
300  return cfg;
301 
302  } else if(is_replay_end && is_mp_game) {
303  DBG_REPLAY << "MP synchronization: remote server choice";
304 
305  // Here we can get into the situation that the decision has already been made but not received yet.
307 
308  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
309  // remote choice.
311 
312  // We don't want to send multiple "require_random" to the server.
313  if(!did_require) {
314  sch.send_request();
315  did_require = true;
316  }
317 
318  SDL_Delay(10);
319  continue;
320 
321  } else if(!is_replay_end) {
322  // The decision has already been made, and must be extracted from the replay.
323  DBG_REPLAY << "MP synchronization: replay server choice";
325 
326  const config* action = resources::recorder->get_next_action();
327  if(!action) {
328  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
330  return sch.local_choice();
331  }
332 
333  if(!action->has_child(sch.name())) {
334  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
335  + action->debug() + "\n");
336 
338  return sch.local_choice();
339  }
340 
341  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
342  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
343  // their attack results in mp" cheat
344  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
345  }
346 
347  config res = action->mandatory_child(sch.name());
348  if(res["request_id"] != sch.request_id()) {
349  WRN_REPLAY << "Unexpected request_id: " << res["request_id"] << " expected: " << sch.request_id();
350  }
351  return res;
352  }
353  }
354 }
355 
357 {
358  undo_commands_.emplace_front(commands, ctx);
359 }
360 
362 {
363  undo_commands_.emplace_front(idx, ctx);
364 }
365 
367 {
368  undo_commands_.emplace_front(idx, args, ctx);
369 }
370 
372  : new_rng_(synced_context::get_rng_for_action())
373  , old_rng_(randomness::generator)
374 {
375  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base";
376 
377  assert(!resources::whiteboard->has_planned_unit_map());
379 
382  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
384 
387 }
388 
390 {
391  LOG_REPLAY << "set_scontext_synced_base:: destructor";
395 }
396 
399  , new_checkup_(generate_checkup("checkup"))
400  , disabler_()
401 {
402  init();
403 }
404 
407  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
408  , disabler_()
409 {
410  init();
411 }
412 
413 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
414 {
415  if(resources::classification->oos_debug) {
416  return new mp_debug_checkup();
417  } else {
418  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
419  }
420 }
421 
422 /*
423  so we don't have to write the same code 3 times.
424 */
426 {
427  LOG_REPLAY << "set_scontext_synced::set_scontext_synced";
428  did_final_checkup_ = false;
431 }
432 
434 {
435  assert(!did_final_checkup_);
436  std::stringstream msg;
437  config co;
438  config cn {
439  "random_calls", new_rng_->get_random_calls(),
440  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
441  };
442 
443  if(checkup_instance->local_checkup(cn, co)) {
444  return;
445  }
446 
447  if(co["random_calls"].empty()) {
448  msg << "cannot find random_calls check in replay" << std::endl;
449  } else if(co["random_calls"] != cn["random_calls"]) {
450  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
451  << co["random_calls"].to_int() << " times." << std::endl;
452  }
453 
454  // Ignore empty next_unit_id to prevent false positives with older saves.
455  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
456  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
457  << co["next_unit_id"].to_int() << std::endl;
458  }
459 
460  if(!msg.str().empty()) {
461  msg << co.debug() << std::endl;
462  if(dont_throw) {
463  ERR_REPLAY << msg.str();
464  } else {
465  replay::process_error(msg.str());
466  }
467  }
468 
469  did_final_checkup_ = true;
470 }
471 
473 {
474  LOG_REPLAY << "set_scontext_synced:: destructor";
475  assert(checkup_instance == &*new_checkup_);
476  if(!did_final_checkup_) {
477  // do_final_checkup(true);
478  }
480 }
481 
483 {
484  return new_rng_->get_random_calls();
485 }
486 
488  : old_rng_(randomness::generator)
489 {
492 
493  // calling the synced rng form inside a local_choice would cause oos.
494  // TODO: should we also reset the synced checkup?
496 }
497 
499 {
503 }
504 
506  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
507 {
508 }
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:212
A class to check whether the results that were calculated in the replay match the results calculated ...
virtual bool local_checkup(const config &expected_data, config &real_data)=0
Compares data to the results calculated during the original game.
Variant for storing WML attributes.
std::string str(const std::string &fallback="") const
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:371
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:321
std::string debug() const
Definition: config.cpp:1248
virtual void play_slice(bool is_delay_enabled=true)
n_unit::id_manager & unit_id_manager()
Definition: game_board.hpp:80
bool end_turn_forced() const
Definition: game_data.hpp:147
A RAII object to temporary leave the synced context like in wesnoth.synchronize_choice.
randomness::rng * old_rng_
This checkup always compares the results in from different clients in a mp game but it also causes mo...
std::size_t get_save_id() const
Used for saving id to savegame.
Definition: id.cpp:42
void maybe_throw_return_to_play_side() const
int get_server_request_number() const
void check_victory()
Checks to see if a side has won.
void increase_server_request_number()
virtual void send_to_wesnothd(const config &, const std::string &="unknown") const
virtual bool is_networked_mp() const
unsigned int get_random_calls() const
Provides the number of random calls to the rng in this context.
Definition: random.cpp:80
static rng & default_instance()
Definition: random.cpp:74
void add_synced_command(const std::string &name, const config &command)
Definition: replay.cpp:248
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition: replay.cpp:258
bool at_end() const
Definition: replay.cpp:633
void revert_action()
Definition: replay.cpp:613
void undo()
Definition: replay.cpp:569
static void process_error(const std::string &msg)
Definition: replay.cpp:199
config * get_next_action()
Definition: replay.cpp:620
std::shared_ptr< randomness::rng > new_rng_
randomness::rng * old_rng_
A RAII object to enter the synced context, cannot be called if we are already in a synced context.
void do_final_checkup(bool dont_throw=false)
static checkup * generate_checkup(const std::string &tagname)
const std::unique_ptr< checkup > new_checkup_
This checkup compares whether the results calculated during the original game match the ones calculat...
static map & registry()
using static function variable instead of static member variable to prevent static initialization fia...
std::function< void(const std::string &)> error_handler_function
virtual config request() const =0
The request which is sent to the mp server.
virtual const char * name() const =0
virtual config local_choice() const =0
We are in a game with no mp server and need to do this choice locally.
static void set_last_unit_id(int id)
static bool undo_blocked()
static std::shared_ptr< randomness::rng > get_rng_for_action()
static config ask_server_choice(const server_choice &)
If we are in a mp game, ask the server, otherwise generate the answer ourselves.
static void reset_is_simultaneous()
Sets is_simultaneous_ = false, called when entering the synced context.
static void add_undo_commands(const config &commands, const game_events::queued_event &ctx)
static event_list undo_commands_
Actions to be executed when the current action is undone.
static void reset_undo_commands()
static void ignore_error_function(const std::string &message)
A function to be passed to run_in_synced_context to ignore the error.
static std::string generate_random_seed()
Generates a new seed for a synced event, by asking the 'server'.
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)
static int get_unit_id_diff()
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).
static void just_log_error_function(const std::string &message)
A function to be passed to run_in_synced_context to log the error.
static synced_state get_synced_state()
static void pull_remote_user_input()
called from get_user_choice while waiting for a remove user choice.
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 'synced', initialises random context, and calls the given function.
static bool is_simultaneous_
As soon as get_user_choice is used with side != current_side (for example in generate_random_seed) ot...
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)
static bool is_unsynced()
static int last_unit_id_
Used to restore the unit id manager when undoing.
static bool is_synced()
static void set_synced_state(synced_state newstate)
Should only be called form set_scontext_synced, set_scontext_local_choice.
static void send_user_choice()
called from get_user_choice to send a recently made choice to the other clients.
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.
static void set_is_simultaneous()
Sets is_simultaneous_ = true, called using a user choice that is not the currently playing side.
static void send_user_choice()
static void pull_remote_choice()
Contains the exception interfaces used to signal completion of a scenario, campaign or turn.
Standard logging facilities (interface).
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:81
rng * generator
This generator is automatically synced during synced context.
Definition: random.cpp:61
game_board * gameboard
Definition: resources.cpp:21
game_data * gamedata
Definition: resources.cpp:23
replay * recorder
Definition: resources.cpp:29
actions::undo_list * undo_stack
Definition: resources.cpp:33
game_classification * classification
Definition: resources.cpp:35
play_controller * controller
Definition: resources.cpp:22
std::shared_ptr< wb::manager > whiteboard
Definition: resources.cpp:34
std::string next_seed_str()
Definition: seed_rng.cpp:37
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:110
std::string_view data
Definition: picture.cpp:199
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:701
Replay control code.
checkup * checkup_instance
#define WRN_REPLAY
#define DBG_REPLAY
static lg::log_domain log_replay("replay")
#define LOG_REPLAY
#define ERR_REPLAY
Various functions that implement the undoing (and redoing) of in-game commands.