The Battle for Wesnoth  1.19.0-dev
synced_context.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2024
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 "log.hpp"
25 #include "play_controller.hpp"
26 #include "random.hpp"
27 #include "random_deterministic.hpp"
28 #include "random_synced.hpp"
29 #include "replay.hpp"
30 #include "resources.hpp"
31 #include "seed_rng.hpp"
32 #include "synced_checkup.hpp"
33 #include "syncmp_handler.hpp"
34 #include "units/id.hpp"
35 #include "whiteboard/manager.hpp"
36 
37 #include <cassert>
38 #include <sstream>
39 
40 static lg::log_domain log_replay("replay");
41 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
42 #define LOG_REPLAY LOG_STREAM(info, log_replay)
43 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
44 #define ERR_REPLAY LOG_STREAM(err, log_replay)
45 
46 bool synced_context::run(const std::string& commandname,
47  const config& data,
48  bool use_undo,
49  bool show,
51 {
52  DBG_REPLAY << "run_in_synced_context:" << commandname;
53 
54  assert(use_undo || (!resources::undo_stack->can_redo() && !resources::undo_stack->can_undo()));
55 
56  // use this after resources::recorder->add_synced_command
57  // because set_scontext_synced sets the checkup to the last added command
59 
61  if(it == synced_command::registry().end()) {
62  error_handler("commandname [" + commandname + "] not found");
63  } else {
64  bool success = it->second(data, use_undo, show, error_handler);
65  if(!success) {
66  return false;
67  }
68  }
69 
71 
72  sync.do_final_checkup();
73 
74  // TODO: It would be nice if this could automaticially detect that
75  // no entry was pushed to the undo stack for this action
76  // and always clear the undo stack in that case.
77  if(undo_blocked()) {
78  // This in particular helps the networking code to make sure this command is sent.
81  }
82 
83  DBG_REPLAY << "run_in_synced_context end";
84  return true;
85 }
86 
87 bool synced_context::run_and_store(const std::string& commandname,
88  const config& data,
89  bool use_undo,
90  bool show,
92 {
93  if(resources::controller->is_replay()) {
94  ERR_REPLAY << "ignored attempt to invoke a synced command during replay";
95  return false;
96  }
97 
98  assert(resources::recorder->at_end());
100  bool success = run(commandname, data, use_undo, show, error_handler);
101  if(!success) {
103  }
104 
105  return success;
106 }
107 
108 bool synced_context::run_and_throw(const std::string& commandname,
109  const config& data,
110  bool use_undo,
111  bool show,
113 {
114  bool success = run_and_store(commandname, data, use_undo, show, error_handler);
115  if(success) {
117  }
118 
119  return success;
120 }
121 
122 bool synced_context::run_in_synced_context_if_not_already(const std::string& commandname,
123  const config& data,
124  bool use_undo,
125  bool show,
127 {
129  case(synced_context::UNSYNCED): {
130  return run_and_throw(commandname, data, use_undo, show, error_handler);
131  }
133  ERR_REPLAY << "trying to execute action while being in a local_choice";
134  // we reject it because such actions usually change the gamestate badly which is not intended during a
135  // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
136  // simultaneously so it could result in invoking different synced commands simultaneously.
137  return false;
138  case(synced_context::SYNCED): {
140  if(it == synced_command::registry().end()) {
141  error_handler("commandname [" + commandname + "] not found");
142  return false;
143  } else {
144  return it->second(data, /*use_undo*/ false, show, error_handler);
145  }
146  }
147  default:
148  assert(false && "found unknown synced_context::synced_state");
149  return false;
150  }
151 }
152 
153 void synced_context::default_error_function(const std::string& message)
154 {
155  ERR_REPLAY << "Unexpected Error during synced execution" << message;
156  assert(!"Unexpected Error during synced execution, more info in stderr.");
157 }
158 
159 void synced_context::just_log_error_function(const std::string& message)
160 {
161  ERR_REPLAY << "Error during synced execution: " << message;
162 }
163 
164 void synced_context::ignore_error_function(const std::string& message)
165 {
166  DBG_REPLAY << "Ignored during synced execution: " << message;
167 }
168 
169 namespace
170 {
171 class random_server_choice : public synced_context::server_choice
172 {
173 public:
174  /** We are in a game with no mp server and need to do this choice locally. */
175  virtual config local_choice() const override
176  {
177  return config{"new_seed", seed_rng::next_seed_str()};
178  }
179 
180  /** The request which is sent to the mp server. */
181  virtual config request() const override
182  {
183  return config();
184  }
185 
186  virtual const char* name() const override
187  {
188  return "random_seed";
189  }
190 };
191 } // namespace
192 
194 {
195  config retv_c = synced_context::ask_server_choice(random_server_choice());
196  config::attribute_value seed_val = retv_c["new_seed"];
197 
198  return seed_val.str();
199 }
200 
202 {
204  is_simultaneous_ = true;
205 }
206 
207 void synced_context::block_undo(bool do_block)
208 {
209  is_undo_blocked_ |= do_block;
211  // Since the action cannot be undone, send it immidiately to the other players.
213 }
214 
216 {
217  // this method should only works in a synced context.
218  assert(!is_unsynced());
219  // if we sent data of this action over the network already, undoing is blocked.
220  // if we called the rng, undoing is blocked.
221  // if the game has ended, undoing is blocked.
222  // if the turn has ended undoing is blocked.
223  return is_simultaneous_
226  || resources::controller->is_regular_game_end()
228 }
229 
231 {
232  // this method only works in a synced context.
233  assert(is_synced());
235 }
236 
238 {
240 }
241 
242 // TODO: this is now also used for normal actions, maybe it should be renamed.
244 {
245  assert(undo_blocked());
247 }
248 
249 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
250 {
251  const std::string& mode = resources::classification->random_mode;
252  if(mode == "deterministic" || mode == "biased") {
253  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
254  } else {
255  return std::make_shared<randomness::synced_rng>(generate_random_seed);
256  }
257 }
258 
260 {
262 }
263 
265 {
267  "request_choice", config {
268  "request_id", request_id(),
269  name(), request(),
270  },
271  });
272 }
273 
275 {
276  if(!is_synced()) {
277  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
278  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.";
279  return sch.local_choice();
280  }
281 
284  const bool is_mp_game = resources::controller->is_networked_mp();
285  bool did_require = false;
286 
287  DBG_REPLAY << "ask_server for random_seed";
288 
289  // As soon as random or similar is involved, undoing is impossible.
291 
292  // There might be speak or similar commands in the replay before the user input.
293  while(true) {
295  bool is_replay_end = resources::recorder->at_end();
296 
297  if(is_replay_end && !is_mp_game) {
298  // The decision is ours, and it will be inserted into the replay.
299  DBG_REPLAY << "MP synchronization: local server choice";
301  config cfg = sch.local_choice();
302  cfg["request_id"] = sch.request_id();
303  //-1 for "server" todo: change that.
304  resources::recorder->user_input(sch.name(), cfg, -1);
305  return cfg;
306 
307  } else if(is_replay_end && is_mp_game) {
308  DBG_REPLAY << "MP synchronization: remote server choice";
309 
310  // Here we can get into the situation that the decision has already been made but not received yet.
312 
313  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
314  // remote choice.
316 
317  // We don't want to send multiple "require_random" to the server.
318  if(!did_require) {
319  sch.send_request();
320  did_require = true;
321  }
322 
323  SDL_Delay(10);
324  continue;
325 
326  } else if(!is_replay_end) {
327  // The decision has already been made, and must be extracted from the replay.
328  DBG_REPLAY << "MP synchronization: replay server choice";
330 
331  const config* action = resources::recorder->get_next_action();
332  if(!action) {
333  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
335  return sch.local_choice();
336  }
337 
338  if(!action->has_child(sch.name())) {
339  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
340  + action->debug() + "\n");
341 
343  return sch.local_choice();
344  }
345 
346  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
347  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
348  // their attack results in mp" cheat
349  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
350  }
351 
352  config res = action->mandatory_child(sch.name());
353  if(res["request_id"] != sch.request_id()) {
354  WRN_REPLAY << "Unexpected request_id: " << res["request_id"] << " expected: " << sch.request_id();
355  }
356  return res;
357  }
358  }
359 }
360 
362 {
363  undo_commands_.emplace_front(commands, ctx);
364 }
365 
367 {
368  undo_commands_.emplace_front(idx, ctx);
369 }
370 
372 {
373  undo_commands_.emplace_front(idx, args, ctx);
374 }
375 
377 {
378  auto& ct = resources::controller->current_team();
379  // Ai doesn't undo stuff, disabling the undo stack allows us to send moves to other clients sooner.
380  return ct.is_ai() && ct.auto_shroud_updates();
381 }
382 
384  : new_rng_(synced_context::get_rng_for_action())
385  , old_rng_(randomness::generator)
386 {
387  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base";
388 
389  assert(!resources::whiteboard->has_planned_unit_map());
391 
395  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
397 
400 }
401 
403 {
404  LOG_REPLAY << "set_scontext_synced_base:: destructor";
408 }
409 
412  , new_checkup_(generate_checkup("checkup"))
413  , disabler_()
414 {
415  init();
416 }
417 
420  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
421  , disabler_()
422 {
423  init();
424 }
425 
426 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
427 {
428  if(resources::classification->oos_debug) {
429  return new mp_debug_checkup();
430  } else {
431  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
432  }
433 }
434 
435 /*
436  so we don't have to write the same code 3 times.
437 */
439 {
440  LOG_REPLAY << "set_scontext_synced::set_scontext_synced";
441  did_final_checkup_ = false;
444 }
445 
447 {
448  assert(!did_final_checkup_);
449  std::stringstream msg;
450  config co;
451  config cn {
452  "random_calls", new_rng_->get_random_calls(),
453  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
454  };
455 
456  if(checkup_instance->local_checkup(cn, co)) {
457  return;
458  }
459 
460  if(co["random_calls"].empty()) {
461  msg << "cannot find random_calls check in replay" << std::endl;
462  } else if(co["random_calls"] != cn["random_calls"]) {
463  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
464  << co["random_calls"].to_int() << " times." << std::endl;
465  }
466 
467  // Ignore empty next_unit_id to prevent false positives with older saves.
468  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
469  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
470  << co["next_unit_id"].to_int() << std::endl;
471  }
472 
473  if(!msg.str().empty()) {
474  msg << co.debug() << std::endl;
475  if(dont_throw) {
476  ERR_REPLAY << msg.str();
477  } else {
478  replay::process_error(msg.str());
479  }
480  }
481 
482  did_final_checkup_ = true;
483 }
484 
486 {
487  LOG_REPLAY << "set_scontext_synced:: destructor";
488  assert(checkup_instance == &*new_checkup_);
489  if(!did_final_checkup_) {
490  // do_final_checkup(true);
491  }
493 }
494 
496 {
497  return new_rng_->get_random_calls();
498 }
499 
501  : old_rng_(randomness::generator)
502 {
505 
506  // calling the synced rng form inside a local_choice would cause oos.
507  // TODO: should we also reset the synced checkup?
509 }
510 
512 {
516 }
517 
519  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
520 {
521 }
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:201
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:159
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:367
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:317
std::string debug() const
Definition: config.cpp:1244
virtual void play_slice(bool is_delay_enabled=true)
n_unit::id_manager & unit_id_manager()
Definition: game_board.hpp:73
bool end_turn_forced() const
Definition: game_data.hpp:146
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
virtual void send_actions()
Sends replay [command]s to the server.
virtual void receive_actions()
Reads and executes replay [command]s from the server.
unsigned int get_random_calls() const
Provides the number of random calls to the rng in this context.
Definition: random.cpp:79
static rng & default_instance()
Definition: random.cpp:73
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:645
void revert_action()
Definition: replay.cpp:614
void undo()
Definition: replay.cpp:570
static void process_error(const std::string &msg)
Definition: replay.cpp:199
config * get_next_action()
Definition: replay.cpp:621
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 bool is_undo_blocked_
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 void reset_block_undo()
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 block_undo(bool do_block=true)
static bool ignore_undo()
bool is_ai() const
Definition: team.hpp:251
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:79
rng * generator
This generator is automatically synced during synced context.
Definition: random.cpp:60
game_board * gameboard
Definition: resources.cpp:20
game_data * gamedata
Definition: resources.cpp:22
replay * recorder
Definition: resources.cpp:28
actions::undo_list * undo_stack
Definition: resources.cpp:32
game_classification * classification
Definition: resources.cpp:34
play_controller * controller
Definition: resources.cpp:21
std::shared_ptr< wb::manager > whiteboard
Definition: resources.cpp:33
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:109
std::string_view data
Definition: picture.cpp:194
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:728
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.