The Battle for Wesnoth  1.19.6+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 "utils/general.hpp"
36 #include "whiteboard/manager.hpp"
37 
38 #include <cassert>
39 #include <sstream>
40 #include <thread>
41 
42 static lg::log_domain log_replay("replay");
43 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
44 #define LOG_REPLAY LOG_STREAM(info, log_replay)
45 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
46 #define ERR_REPLAY LOG_STREAM(err, log_replay)
47 
48 bool synced_context::run(const std::string& commandname, const config& data, action_spectator& spectator)
49 {
50  DBG_REPLAY << "run_in_synced_context:" << commandname;
51 
52  // use this after resources::recorder->add_synced_command
53  // because set_scontext_synced sets the checkup to the last added command
56 
57  auto p_handler = utils::find(synced_command::registry(), commandname);
58  if(!p_handler) {
59  spectator.error("commandname [" + commandname + "] not found");
60  } else {
61  bool success = p_handler->second(data, spectator);
62  if(!success) {
63  return false;
64  }
65  }
66 
68 
69  sync.do_final_checkup();
70 
72 
73  if(undo_blocked()) {
74  // This in particular helps the networking code to make sure this command is sent.
76  }
77 
78  DBG_REPLAY << "run_in_synced_context end";
79  return true;
80 }
81 
82 bool synced_context::run_and_store(const std::string& commandname, const config& data, action_spectator& spectator)
83 {
84  if(resources::controller->is_replay()) {
85  ERR_REPLAY << "ignored attempt to invoke a synced command during replay";
86  return false;
87  }
88 
89  assert(resources::recorder->at_end());
91  bool success = run(commandname, data, spectator);
92  if(!success) {
94  } else {
96  }
97  return success;
98 }
99 
100 bool synced_context::run_and_throw(const std::string& commandname, const config& data, action_spectator& spectator)
101 {
102  bool success = run_and_store(commandname, data, spectator);
103  if(success) {
105  }
106 
107  return success;
108 }
109 
111  const std::string& commandname, const config& data, action_spectator& spectator)
112 {
114  case(synced_context::UNSYNCED): {
115  return run_and_throw(commandname, data, spectator);
116  }
118  ERR_REPLAY << "trying to execute action while being in a local_choice";
119  // we reject it because such actions usually change the gamestate badly which is not intended during a
120  // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
121  // simultaneously so it could result in invoking different synced commands simultaneously.
122  return false;
123  case(synced_context::SYNCED): {
125  if(it == synced_command::registry().end()) {
126  spectator.error("commandname [" + commandname + "] not found");
127  return false;
128  } else {
129  return it->second(data, spectator);
130  }
131  }
132  default:
133  assert(false && "found unknown synced_context::synced_state");
134  return false;
135  }
136 }
137 
139 {
140  static class : public action_spectator
141  {
142  public:
143  void error(const std::string& message)
144  {
145  ERR_REPLAY << "Unexpected Error during synced execution" << message;
146  assert(!"Unexpected Error during synced execution, more info in stderr.");
147  }
148 
149  } res;
150  return res;
151 }
152 
153 namespace
154 {
155 class random_server_choice : public synced_context::server_choice
156 {
157 public:
158  /** We are in a game with no mp server and need to do this choice locally. */
159  virtual config local_choice() const override
160  {
161  return config{"new_seed", seed_rng::next_seed_str()};
162  }
163 
164  /** The request which is sent to the mp server. */
165  virtual config request() const override
166  {
167  return config();
168  }
169 
170  virtual const char* name() const override
171  {
172  return "random_seed";
173  }
174 };
175 } // namespace
176 
178 {
179  config retv_c = synced_context::ask_server_choice(random_server_choice());
180  config::attribute_value seed_val = retv_c["new_seed"];
181 
182  return seed_val.str();
183 }
184 
185 void synced_context::block_undo(bool do_block, bool clear_undo)
186 {
187  if(!do_block) {
188  return;
189  }
190  is_undo_blocked_ = true;
191 
192  if(clear_undo) {
194  }
195  // Since the action cannot be undone, send it immidiately to the other players.
197 }
198 
200 {
201  // this method only works in a synced context.
202  assert(!is_unsynced());
203  // if we sent data of this action over the network already, undoing is blocked.
204  // if the game has ended, undoing is blocked.
205  // if the turn has ended undoing is blocked.
206 
207  // Important: once this function returned true, it has to return true for the rest of the duration of the current action
208  // otherwise OOS happens, so the following code in particular relies on the inability to revoke a [end_turn]/[endlevel]
209  return is_undo_blocked_
212 }
213 
215 {
216  // this method only works in a synced context.
217  assert(is_synced());
219 }
220 
222 {
224 }
225 
226 // TODO: this is now also used for normal actions, maybe it should be renamed.
228 {
229  assert(undo_blocked());
231 }
232 
233 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
234 {
235  const std::string& mode = resources::classification->random_mode;
236  if(mode == "deterministic" || mode == "biased") {
237  auto get_rng = []() {
238  //rnd is nonundoable, even when the deterministic rng is used.
239  synced_context::block_undo(true, false);
241  };
242  return std::make_shared<randomness::rng_proxy>(get_rng);
243  } else {
244  return std::make_shared<randomness::synced_rng>(generate_random_seed);
245  }
246 }
247 
249 {
251 }
252 
254 {
256  "request_choice", config {
257  "request_id", request_id(),
258  name(), request(),
259  },
260  });
261 }
262 
264 {
265  if(!is_synced()) {
266  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
267  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.";
268  return sch.local_choice();
269  }
270 
271  block_undo(true, false);
273  const bool is_mp_game = resources::controller->is_networked_mp();
274  bool did_require = false;
275 
276  DBG_REPLAY << "ask_server for random_seed";
277 
278  // There might be speak or similar commands in the replay before the user input.
279  while(true) {
281  bool is_replay_end = resources::recorder->at_end();
282 
283  if(is_replay_end && !is_mp_game) {
284  // The decision is ours, and it will be inserted into the replay.
285  DBG_REPLAY << "MP synchronization: local server choice";
287  config cfg = sch.local_choice();
288  cfg["request_id"] = sch.request_id();
289  //-1 for "server" todo: change that.
290  resources::recorder->user_input(sch.name(), cfg, -1);
291  return cfg;
292 
293  } else if(is_replay_end && is_mp_game) {
294  DBG_REPLAY << "MP synchronization: remote server choice";
295 
296  // Here we can get into the situation that the decision has already been made but not received yet.
298 
299  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
300  // remote choice.
302 
303  // We don't want to send multiple "require_random" to the server.
304  if(!did_require) {
305  sch.send_request();
306  did_require = true;
307  }
308 
309  using namespace std::chrono_literals;
310  std::this_thread::sleep_for(10ms);
311  continue;
312 
313  } else if(!is_replay_end) {
314  // The decision has already been made, and must be extracted from the replay.
315  DBG_REPLAY << "MP synchronization: replay server choice";
317 
318  const config* action = resources::recorder->get_next_action();
319  if(!action) {
320  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
322  return sch.local_choice();
323  }
324 
325  if(!action->has_child(sch.name())) {
326  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
327  + action->debug() + "\n");
328 
330  return sch.local_choice();
331  }
332 
333  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
334  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
335  // their attack results in mp" cheat
336  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
337  }
338 
339  config res = action->mandatory_child(sch.name());
340  if(res["request_id"].to_int() != sch.request_id()) {
341  WRN_REPLAY << "Unexpected request_id: " << res["request_id"] << " expected: " << sch.request_id();
342  }
343  return res;
344  }
345  }
346 }
347 
349 {
350  auto& ct = resources::controller->current_team();
351  // Ai doesn't undo stuff, disabling the undo stack allows us to send moves to other clients sooner.
352  return ct.is_ai() && ct.auto_shroud_updates();
353 }
354 
356  : new_rng_(synced_context::get_rng_for_action())
357  , old_rng_(randomness::generator)
358 {
359  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base";
360 
361  assert(!resources::whiteboard->has_planned_unit_map());
363 
366  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
367 
370 }
371 
373 {
374  LOG_REPLAY << "set_scontext_synced_base:: destructor";
378 }
379 
382  , new_checkup_(generate_checkup("checkup"))
383  , disabler_()
384 {
385  init();
386 }
387 
390  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
391  , disabler_()
392 {
393  init();
394 }
395 
396 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
397 {
398  if(resources::classification->oos_debug) {
399  return new mp_debug_checkup();
400  } else {
401  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
402  }
403 }
404 
405 /*
406  so we don't have to write the same code 3 times.
407 */
409 {
410  LOG_REPLAY << "set_scontext_synced::set_scontext_synced";
411  did_final_checkup_ = false;
414 }
415 
417 {
418  assert(!did_final_checkup_);
419  std::stringstream msg;
420  config co;
421  config cn {
422  "random_calls", new_rng_->get_random_calls(),
423  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
424  };
425 
426  if(checkup_instance->local_checkup(cn, co)) {
427  return;
428  }
429 
430  if(co["random_calls"].empty()) {
431  msg << "cannot find random_calls check in replay" << std::endl;
432  } else if(co["random_calls"] != cn["random_calls"]) {
433  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
434  << co["random_calls"].to_int() << " times." << std::endl;
435  }
436 
437  // Ignore empty next_unit_id to prevent false positives with older saves.
438  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
439  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
440  << co["next_unit_id"].to_int() << std::endl;
441  }
442 
443  if(!msg.str().empty()) {
444  msg << co.debug() << std::endl;
445  if(dont_throw) {
446  ERR_REPLAY << msg.str();
447  } else {
448  replay::process_error(msg.str());
449  }
450  }
451 
452  did_final_checkup_ = true;
453 }
454 
456 {
457  LOG_REPLAY << "set_scontext_synced:: destructor";
458  assert(checkup_instance == &*new_checkup_);
459  if(!did_final_checkup_) {
460  // do_final_checkup(true);
461  }
463 }
464 
466 {
467  return new_rng_->get_random_calls();
468 }
469 
471  : old_rng_(randomness::generator)
472 {
475 
476  // calling the synced rng form inside a local_choice would cause oos.
477  // TODO: should we also reset the synced checkup?
479 }
480 
482 {
486 }
487 
489  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
490 {
491 }
virtual void error(const std::string &message)
Called when synced_context::run received nonsensial data based on the current gamestate.
void cleanup_action()
called after a user action, removes empty actions
Definition: undo.cpp:269
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:131
void init_action()
called before a user action, starts collecting undo steps for the new action.
Definition: undo.cpp:251
void finish_action(bool can_undo)
called after a user action, pushes the collected undo steps on the undo stack.
Definition: undo.cpp:258
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:172
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:366
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:316
std::string debug() const
Definition: config.cpp:1240
virtual void play_slice()
n_unit::id_manager & unit_id_manager()
Definition: game_board.hpp:74
bool end_turn_forced() const
Definition: game_data.hpp:146
const randomness::mt_rng & rng() const
Definition: game_data.hpp:67
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()
bool is_regular_game_end() const
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.
uint32_t get_next_random()
Get a new random number.
Definition: mt_rng.cpp:63
static rng & default_instance()
Definition: random.cpp:73
void add_synced_command(const std::string &name, const config &command)
Definition: replay.cpp:233
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition: replay.cpp:243
bool at_end() const
Definition: replay.cpp:630
void revert_action()
Definition: replay.cpp:599
void undo()
Definition: replay.cpp:555
static void process_error(const std::string &msg)
Definition: replay.cpp:184
config * get_next_action()
Definition: replay.cpp:606
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...
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 run_in_synced_context_if_not_already(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
Checks whether we are currently running in a synced context, and if not we enters it.
static bool is_undo_blocked_
As soon as get_user_choice is used with side != current_side (for example in generate_random_seed) ot...
static bool run(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
Sets the context to 'synced', initialises random context, and calls the given function.
static std::string generate_random_seed()
Generates a new seed for a synced event, by asking the 'server'.
static int get_unit_id_diff()
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 is_unsynced()
static void block_undo(bool do_block=true, bool clear_undo=true)
set this to false to prevent clearing the undo stack, this is important when we cannot change the gam...
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_and_store(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
static action_spectator & get_default_spectator()
An object to be passed to run_in_synced_context to assert false on error (the default).
static bool ignore_undo()
static bool run_and_throw(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
bool is_ai() const
Definition: team.hpp:251
Definitions for the interface to Wesnoth Markup Language (WML).
Standard logging facilities (interface).
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
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:140
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:178
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:708
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.