The Battle for Wesnoth  1.19.5+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 #include <thread>
40 
41 static lg::log_domain log_replay("replay");
42 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
43 #define LOG_REPLAY LOG_STREAM(info, log_replay)
44 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
45 #define ERR_REPLAY LOG_STREAM(err, log_replay)
46 
47 bool synced_context::run(const std::string& commandname, const config& data, action_spectator& spectator)
48 {
49  DBG_REPLAY << "run_in_synced_context:" << commandname;
50 
51  // use this after resources::recorder->add_synced_command
52  // because set_scontext_synced sets the checkup to the last added command
54 
56  if(it == synced_command::registry().end()) {
57  spectator.error("commandname [" + commandname + "] not found");
58  } else {
59  bool success = it->second(data, spectator);
60  if(!success) {
61  return false;
62  }
63  }
64 
66 
67  sync.do_final_checkup();
68 
69  // TODO: It would be nice if this could automaticially detect that
70  // no entry was pushed to the undo stack for this action
71  // and always clear the undo stack in that case.
72  if(undo_blocked()) {
73  // 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  }
95 
96  return success;
97 }
98 
99 bool synced_context::run_and_throw(const std::string& commandname, const config& data, action_spectator& spectator)
100 {
101  bool success = run_and_store(commandname, data, spectator);
102  if(success) {
104  }
105 
106  return success;
107 }
108 
110  const std::string& commandname, const config& data, action_spectator& spectator)
111 {
113  case(synced_context::UNSYNCED): {
114  return run_and_throw(commandname, data, spectator);
115  }
117  ERR_REPLAY << "trying to execute action while being in a local_choice";
118  // we reject it because such actions usually change the gamestate badly which is not intended during a
119  // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
120  // simultaneously so it could result in invoking different synced commands simultaneously.
121  return false;
122  case(synced_context::SYNCED): {
124  if(it == synced_command::registry().end()) {
125  spectator.error("commandname [" + commandname + "] not found");
126  return false;
127  } else {
128  return it->second(data, spectator);
129  }
130  }
131  default:
132  assert(false && "found unknown synced_context::synced_state");
133  return false;
134  }
135 }
136 
138 {
139  static class : public action_spectator
140  {
141  public:
142  void error(const std::string& message)
143  {
144  ERR_REPLAY << "Unexpected Error during synced execution" << message;
145  assert(!"Unexpected Error during synced execution, more info in stderr.");
146  }
147 
148  } res;
149  return res;
150 }
151 
152 namespace
153 {
154 class random_server_choice : public synced_context::server_choice
155 {
156 public:
157  /** We are in a game with no mp server and need to do this choice locally. */
158  virtual config local_choice() const override
159  {
160  return config{"new_seed", seed_rng::next_seed_str()};
161  }
162 
163  /** The request which is sent to the mp server. */
164  virtual config request() const override
165  {
166  return config();
167  }
168 
169  virtual const char* name() const override
170  {
171  return "random_seed";
172  }
173 };
174 } // namespace
175 
177 {
178  config retv_c = synced_context::ask_server_choice(random_server_choice());
179  config::attribute_value seed_val = retv_c["new_seed"];
180 
181  return seed_val.str();
182 }
183 
184 void synced_context::block_undo(bool do_block, bool clear_undo)
185 {
186  if(!do_block) {
187  return;
188  }
189  is_undo_blocked_ = true;
190 
191  if(clear_undo) {
193  }
194  // Since the action cannot be undone, send it immidiately to the other players.
196 }
197 
199 {
200  // this method only works in a synced context.
201  assert(!is_unsynced());
202  // if we sent data of this action over the network already, undoing is blocked.
203  // if the game has ended, undoing is blocked.
204  // if the turn has ended undoing is blocked.
205 
206  // Important: once this function returned true, it has to return true for the rest of the duration of the current action
207  // otherwise OOS happens, so the following code in particular relies on the inability to revoke a [end_turn]/[endlevel]
208  return is_undo_blocked_
211 }
212 
214 {
215  // this method only works in a synced context.
216  assert(is_synced());
218 }
219 
221 {
223 }
224 
225 // TODO: this is now also used for normal actions, maybe it should be renamed.
227 {
228  assert(undo_blocked());
230 }
231 
232 std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
233 {
234  const std::string& mode = resources::classification->random_mode;
235  if(mode == "deterministic" || mode == "biased") {
236  auto get_rng = []() {
237  //rnd is nonundoable, even when the deterministic rng is used.
238  synced_context::block_undo(true, false);
240  };
241  return std::make_shared<randomness::rng_proxy>(get_rng);
242  } else {
243  return std::make_shared<randomness::synced_rng>(generate_random_seed);
244  }
245 }
246 
248 {
250 }
251 
253 {
255  "request_choice", config {
256  "request_id", request_id(),
257  name(), request(),
258  },
259  });
260 }
261 
263 {
264  if(!is_synced()) {
265  ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
266  << "' choice in a unsynced context, doing the choice locally. This can cause OOS.";
267  return sch.local_choice();
268  }
269 
270  block_undo(true, false);
272  const bool is_mp_game = resources::controller->is_networked_mp();
273  bool did_require = false;
274 
275  DBG_REPLAY << "ask_server for random_seed";
276 
277  // There might be speak or similar commands in the replay before the user input.
278  while(true) {
280  bool is_replay_end = resources::recorder->at_end();
281 
282  if(is_replay_end && !is_mp_game) {
283  // The decision is ours, and it will be inserted into the replay.
284  DBG_REPLAY << "MP synchronization: local server choice";
286  config cfg = sch.local_choice();
287  cfg["request_id"] = sch.request_id();
288  //-1 for "server" todo: change that.
289  resources::recorder->user_input(sch.name(), cfg, -1);
290  return cfg;
291 
292  } else if(is_replay_end && is_mp_game) {
293  DBG_REPLAY << "MP synchronization: remote server choice";
294 
295  // Here we can get into the situation that the decision has already been made but not received yet.
297 
298  // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
299  // remote choice.
301 
302  // We don't want to send multiple "require_random" to the server.
303  if(!did_require) {
304  sch.send_request();
305  did_require = true;
306  }
307 
308  using namespace std::chrono_literals;
309  std::this_thread::sleep_for(10ms);
310  continue;
311 
312  } else if(!is_replay_end) {
313  // The decision has already been made, and must be extracted from the replay.
314  DBG_REPLAY << "MP synchronization: replay server choice";
316 
317  const config* action = resources::recorder->get_next_action();
318  if(!action) {
319  replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
321  return sch.local_choice();
322  }
323 
324  if(!action->has_child(sch.name())) {
325  replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
326  + action->debug() + "\n");
327 
329  return sch.local_choice();
330  }
331 
332  if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
333  // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
334  // their attack results in mp" cheat
335  replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
336  }
337 
338  config res = action->mandatory_child(sch.name());
339  if(res["request_id"].to_int() != sch.request_id()) {
340  WRN_REPLAY << "Unexpected request_id: " << res["request_id"] << " expected: " << sch.request_id();
341  }
342  return res;
343  }
344  }
345 }
346 
348 {
349  undo_commands_.emplace_front(commands, ctx);
350 }
351 
353 {
354  undo_commands_.emplace_front(idx, ctx);
355 }
356 
358 {
359  undo_commands_.emplace_front(idx, args, ctx);
360 }
361 
363 {
364  auto& ct = resources::controller->current_team();
365  // Ai doesn't undo stuff, disabling the undo stack allows us to send moves to other clients sooner.
366  return ct.is_ai() && ct.auto_shroud_updates();
367 }
368 
370  : new_rng_(synced_context::get_rng_for_action())
371  , old_rng_(randomness::generator)
372 {
373  LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base";
374 
375  assert(!resources::whiteboard->has_planned_unit_map());
377 
380  synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
382 
385 }
386 
388 {
389  LOG_REPLAY << "set_scontext_synced_base:: destructor";
393 }
394 
397  , new_checkup_(generate_checkup("checkup"))
398  , disabler_()
399 {
400  init();
401 }
402 
405  , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
406  , disabler_()
407 {
408  init();
409 }
410 
411 checkup* set_scontext_synced::generate_checkup(const std::string& tagname)
412 {
413  if(resources::classification->oos_debug) {
414  return new mp_debug_checkup();
415  } else {
416  return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
417  }
418 }
419 
420 /*
421  so we don't have to write the same code 3 times.
422 */
424 {
425  LOG_REPLAY << "set_scontext_synced::set_scontext_synced";
426  did_final_checkup_ = false;
429 }
430 
432 {
433  assert(!did_final_checkup_);
434  std::stringstream msg;
435  config co;
436  config cn {
437  "random_calls", new_rng_->get_random_calls(),
438  "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
439  };
440 
441  if(checkup_instance->local_checkup(cn, co)) {
442  return;
443  }
444 
445  if(co["random_calls"].empty()) {
446  msg << "cannot find random_calls check in replay" << std::endl;
447  } else if(co["random_calls"] != cn["random_calls"]) {
448  msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
449  << co["random_calls"].to_int() << " times." << std::endl;
450  }
451 
452  // Ignore empty next_unit_id to prevent false positives with older saves.
453  if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
454  msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
455  << co["next_unit_id"].to_int() << std::endl;
456  }
457 
458  if(!msg.str().empty()) {
459  msg << co.debug() << std::endl;
460  if(dont_throw) {
461  ERR_REPLAY << msg.str();
462  } else {
463  replay::process_error(msg.str());
464  }
465  }
466 
467  did_final_checkup_ = true;
468 }
469 
471 {
472  LOG_REPLAY << "set_scontext_synced:: destructor";
473  assert(checkup_instance == &*new_checkup_);
474  if(!did_final_checkup_) {
475  // do_final_checkup(true);
476  }
478 }
479 
481 {
482  return new_rng_->get_random_calls();
483 }
484 
486  : old_rng_(randomness::generator)
487 {
490 
491  // calling the synced rng form inside a local_choice would cause oos.
492  // TODO: should we also reset the synced checkup?
494 }
495 
497 {
501 }
502 
504  : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
505 {
506 }
virtual void error(const std::string &message)
Called when synced_context::run received nonsensial data based on the current gamestate.
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:199
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:249
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition: replay.cpp:259
bool at_end() const
Definition: replay.cpp:646
void revert_action()
Definition: replay.cpp:615
void undo()
Definition: replay.cpp:571
static void process_error(const std::string &msg)
Definition: replay.cpp:200
config * get_next_action()
Definition: replay.cpp:622
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 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 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
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:724
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.