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 
208 {
209  // this method should only works in a synced context.
210  assert(!is_unsynced());
211  // if we sent data of this action over the network already, undoing is blocked.
212  // if we called the rng, undoing is blocked.
213  // if the game has ended, undoing is blocked.
214  // if the turn has ended undoing is blocked.
215  return is_simultaneous_
217  || resources::controller->is_regular_game_end()
219 }
220 
222 {
223  // this method only works in a synced context.
224  assert(is_synced());
226 }
227 
229 {
231 }
232 
233 // TODO: this is now also used for normal actions, maybe it should be renamed.
235 {
236  assert(undo_blocked());
238 }
239 
240 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
241 {
242  const std::string& mode = resources::classification->random_mode;
243  if(mode == "deterministic" || mode == "biased") {
244  return std::make_shared<randomness::rng_deterministic>(resources::gamedata->rng());
245  } else {
246  return std::make_shared<randomness::synced_rng>(generate_random_seed);
247  }
248 }
249 
251 {
253 }
254 
256 {
258  "request_choice", config {
259  "request_id", request_id(),
260  name(), request(),
261  },
262  });
263 }
264 
266 {
267  if(!is_synced()) {
268  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
269  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.";
270  return sch.local_choice();
271  }
272 
275  const bool is_mp_game = resources::controller->is_networked_mp();
276  bool did_require = false;
277 
278  DBG_REPLAY << "ask_server for random_seed";
279 
280  // As soon as random or similar is involved, undoing is impossible.
282 
283  // There might be speak or similar commands in the replay before the user input.
284  while(true) {
286  bool is_replay_end = resources::recorder->at_end();
287 
288  if(is_replay_end && !is_mp_game) {
289  // The decision is ours, and it will be inserted into the replay.
290  DBG_REPLAY << "MP synchronization: local server choice";
292  config cfg = sch.local_choice();
293  cfg["request_id"] = sch.request_id();
294  //-1 for "server" todo: change that.
295  resources::recorder->user_input(sch.name(), cfg, -1);
296  return cfg;
297 
298  } else if(is_replay_end && is_mp_game) {
299  DBG_REPLAY << "MP synchronization: remote server choice";
300 
301  // Here we can get into the situation that the decision has already been made but not received yet.
303 
304  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
305  // remote choice.
307 
308  // We don't want to send multiple "require_random" to the server.
309  if(!did_require) {
310  sch.send_request();
311  did_require = true;
312  }
313 
314  SDL_Delay(10);
315  continue;
316 
317  } else if(!is_replay_end) {
318  // The decision has already been made, and must be extracted from the replay.
319  DBG_REPLAY << "MP synchronization: replay server choice";
321 
322  const config* action = resources::recorder->get_next_action();
323  if(!action) {
324  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
326  return sch.local_choice();
327  }
328 
329  if(!action->has_child(sch.name())) {
330  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
331  + action->debug() + "\n");
332 
334  return sch.local_choice();
335  }
336 
337  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
338  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
339  // their attack results in mp" cheat
340  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
341  }
342 
343  config res = action->mandatory_child(sch.name());
344  if(res["request_id"] != sch.request_id()) {
345  WRN_REPLAY << "Unexpected request_id: " << res["request_id"] << " expected: " << sch.request_id();
346  }
347  return res;
348  }
349  }
350 }
351 
353 {
354  undo_commands_.emplace_front(commands, ctx);
355 }
356 
358 {
359  undo_commands_.emplace_front(idx, ctx);
360 }
361 
363 {
364  undo_commands_.emplace_front(idx, args, ctx);
365 }
366 
368  : new_rng_(synced_context::get_rng_for_action())
369  , old_rng_(randomness::generator)
370 {
371  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base";
372 
373  assert(!resources::whiteboard->has_planned_unit_map());
375 
378  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
380 
383 }
384 
386 {
387  LOG_REPLAY << "set_scontext_synced_base:: destructor";
391 }
392 
395  , new_checkup_(generate_checkup("checkup"))
396  , disabler_()
397 {
398  init();
399 }
400 
403  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
404  , disabler_()
405 {
406  init();
407 }
408 
409 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
410 {
411  if(resources::classification->oos_debug) {
412  return new mp_debug_checkup();
413  } else {
414  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
415  }
416 }
417 
418 /*
419  so we don't have to write the same code 3 times.
420 */
422 {
423  LOG_REPLAY << "set_scontext_synced::set_scontext_synced";
424  did_final_checkup_ = false;
427 }
428 
430 {
431  assert(!did_final_checkup_);
432  std::stringstream msg;
433  config co;
434  config cn {
435  "random_calls", new_rng_->get_random_calls(),
436  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
437  };
438 
439  if(checkup_instance->local_checkup(cn, co)) {
440  return;
441  }
442 
443  if(co["random_calls"].empty()) {
444  msg << "cannot find random_calls check in replay" << std::endl;
445  } else if(co["random_calls"] != cn["random_calls"]) {
446  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
447  << co["random_calls"].to_int() << " times." << std::endl;
448  }
449 
450  // Ignore empty next_unit_id to prevent false positives with older saves.
451  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
452  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
453  << co["next_unit_id"].to_int() << std::endl;
454  }
455 
456  if(!msg.str().empty()) {
457  msg << co.debug() << std::endl;
458  if(dont_throw) {
459  ERR_REPLAY << msg.str();
460  } else {
461  replay::process_error(msg.str());
462  }
463  }
464 
465  did_final_checkup_ = true;
466 }
467 
469 {
470  LOG_REPLAY << "set_scontext_synced:: destructor";
471  assert(checkup_instance == &*new_checkup_);
472  if(!did_final_checkup_) {
473  // do_final_checkup(true);
474  }
476 }
477 
479 {
480  return new_rng_->get_random_calls();
481 }
482 
484  : old_rng_(randomness::generator)
485 {
488 
489  // calling the synced rng form inside a local_choice would cause oos.
490  // TODO: should we also reset the synced checkup?
492 }
493 
495 {
499 }
500 
502  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
503 {
504 }
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
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:247
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition: replay.cpp:257
bool at_end() const
Definition: replay.cpp:632
void revert_action()
Definition: replay.cpp:612
void undo()
Definition: replay.cpp:568
static void process_error(const std::string &msg)
Definition: replay.cpp:198
config * get_next_action()
Definition: replay.cpp:619
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()
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:700
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.