The Battle for Wesnoth  1.19.5+dev
synced_user_choice.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2015 - 2024
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 
16 #include "synced_user_choice.hpp"
17 
18 #include "display.hpp"
19 #include "floating_label.hpp"
20 #include "game_data.hpp"
21 #include "log.hpp"
22 #include "play_controller.hpp"
23 #include "synced_context.hpp"
24 #include "replay.hpp"
25 #include "resources.hpp"
27 #include <chrono>
28 #include <set>
29 #include <map>
30 #include "formula/string_utils.hpp"
31 #include "font/standard_colors.hpp"
32 
33 static lg::log_domain log_replay("replay");
34 #define DBG_REPLAY LOG_STREAM(debug, log_replay)
35 #define LOG_REPLAY LOG_STREAM(info, log_replay)
36 #define WRN_REPLAY LOG_STREAM(warn, log_replay)
37 #define ERR_REPLAY LOG_STREAM(err, log_replay)
38 
39 namespace
40 {
41  using namespace std::chrono_literals;
42  class user_choice_notifer_ingame
43  {
44  //the handle for the label on the screen -1 if not shown yet.
45  int label_id_;
46  std::string message_;
47  std::chrono::steady_clock::time_point start_show_;
48 
49  public:
50  user_choice_notifer_ingame()
51  : label_id_(-1)
52  , message_()
53  , start_show_(std::chrono::steady_clock::now() + 2000ms)
54  {
55 
56  }
57 
58  ~user_choice_notifer_ingame()
59  {
60  if(label_id_ != -1) {
61  end_show_label();
62  }
63  }
64 
65  void update(const std::string& message)
66  {
67  if(label_id_ == -1 && std::chrono::steady_clock::now() > start_show_)
68  {
69  start_show_label();
70  }
71  if(message == message_) {
72  return;
73  }
74  message_ = message;
75  if(label_id_ != -1) {
76  end_show_label();
77  start_show_label();
78  }
79  }
80 
81  void start_show_label()
82  {
83  assert(label_id_ == -1);
84  SDL_Rect area = display::get_singleton()->map_outside_area();
85  font::floating_label flabel(message_);
88  flabel.set_position(area.w/2, area.h/4);
89  flabel.set_lifetime(std::chrono::milliseconds{-1});
90  flabel.set_clip_rect(area);
91  label_id_ = font::add_floating_label(flabel);
92  }
93 
94  void end_show_label()
95  {
96  assert(label_id_ != -1);
97  font::remove_floating_label(label_id_);
98  label_id_ = -1;
99  }
100  };
101 }
102 
103 std::map<int,config> mp_sync::get_user_choice_multiple_sides(const std::string &name, const mp_sync::user_choice &uch,
104  std::set<int> sides)
105 {
106  //pass sides by copy because we need a copy.
107  const bool is_synced = synced_context::is_synced();
108  const int max_side = static_cast<int>(resources::gameboard->teams().size());
109  //we currently don't check for too early because lua's sync choice doesn't necessarily show screen dialogs.
110  //It (currently) in the responsibility of the user of sync choice to not use dialogs during prestart events..
111  if(!is_synced)
112  {
113  //we got called from inside lua's wesnoth.synchronize_choice or from a select event.
114  replay::process_error("MP synchronization only works in a synced context (for example Select or preload events are no synced context).\n");
115  return std::map<int,config>();
116  }
117 
118  for(int side : sides)
119  {
120  if(1 > side || side > max_side)
121  {
122  replay::process_error("MP synchronization with an invalid side number.\n");
123  return std::map<int,config>();
124  }
125  }
126 
127  /*
128  for empty sides we want to use random choice instead.
129  */
130  std::set<int> empty_sides;
131  for(int side : sides)
132  {
133  if( resources::gameboard->get_team(side).is_empty())
134  {
135  empty_sides.insert(side);
136  }
137  }
138 
139  for(int side : empty_sides)
140  {
141  sides.erase(side);
142  }
143 
144  std::map<int,config> retv = user_choice_manager::get_user_choice_internal(name, uch, sides);
145 
146  for(int side : empty_sides)
147  {
148  retv[side] = uch.random_choice(side);
149  }
150  return retv;
151 
152 }
153 
154 /*
155  fixes some rare cases and calls get_user_choice_internal if we are in a synced context.
156 */
157 config mp_sync::get_user_choice(const std::string &name, const mp_sync::user_choice &uch,
158  int side)
159 {
160  const bool is_too_early = resources::gamedata->is_before_screen();
161  const bool is_synced = synced_context::is_synced();
162  const bool is_mp_game = resources::controller->is_networked_mp();//Only used in debugging output below
163  const int max_side = static_cast<int>(resources::gameboard->teams().size());
164  bool is_side_null_controlled;
165 
166  /* side = 0 should default to the currently active side per definition. */
167  if(side < 1 || max_side < side) {
168  if(side != 0) {
169  ERR_REPLAY << "Invalid parameter for side in get_user_choice.";
170  }
171 
173  LOG_REPLAY << " side changed to " << side;
174  }
175 
176  if(!is_synced)
177  {
178  //we got called from inside lua's wesnoth.synchronize_choice or from a select event (or maybe a preload event?).
179  //This doesn't cause problems and someone could use it for example to use a [message][option] inside a wesnoth.synchronize_choice which could be useful,
180  //so just give a warning.
181  LOG_REPLAY << "MP synchronization called during an unsynced context.";
182  return uch.query_user(side);
183  }
184  if(is_too_early && uch.is_visible())
185  {
186  //We are in a prestart event or even earlier.
187  //Although we are able to sync them, we cannot use query_user,
188  //because we cannot (or shouldn't) put things on the screen inside a prestart event, this is true for SP and MP games.
189  //Quotation form event wiki: "For things displayed on-screen such as character dialog, use start instead"
190 
191  //Note: it seems like get_user_choice_multiple_sides doesn't reject this case.
192 
193  return uch.random_choice(side);
194  }
195  //in start events it's unclear to decide on which side the function should be executed (default= side1 still).
196  //But for advancements we can just decide on the side that owns the unit and that's in the responsibility of advance_unit_at.
197  //For [message][option] and lua's sync_choice the scenario designer is responsible for that.
198  //For [get_global_variable] side is never null.
199 
200  is_side_null_controlled = resources::gameboard->get_team(side).is_empty();
201 
202  LOG_REPLAY << "get_user_choice_called with"
203  << " name=" << name
204  << " is_synced=" << is_synced
205  << " is_mp_game=" << is_mp_game
206  << " is_side_null_controlled=" << is_side_null_controlled;
207 
208  if (is_side_null_controlled)
209  {
210  DBG_REPLAY << "MP synchronization: side 1 being null-controlled in get_user_choice.";
211  //most likely we are in a start event with an empty side 1
212  //but calling [set_global_variable] to an empty side might also cause this.
213  //i think in that case we should better use uch.random_choice(),
214  //which could return something like config {"invalid", true};
215  side = 1;
216  while ( side <= max_side && resources::gameboard->get_team(side).is_empty() )
217  side++;
218  assert(side <= max_side);
219  }
220 
221 
222  assert(1 <= side && side <= max_side);
223 
224  std::set<int> sides;
225  sides.insert(side);
226  std::map<int, config> retv = user_choice_manager::get_user_choice_internal(name, uch, sides);
227  if(retv.find(side) == retv.end())
228  {
229  //An error occurred, get_user_choice_internal should have given an oos error message
230  return config();
231  }
232  return retv[side];
233 }
234 
235 user_choice_manager::user_choice_manager(const std::string &name, const mp_sync::user_choice &uch, const std::set<int>& sides)
236  : required_(sides)
237  , res_()
238  , local_choice_(0)
239  , wait_message_()
240  , oos_(false)
241  , uch_(uch)
242  , tagname_(name)
243  , current_side_(resources::controller->current_side())
244  , changed_event_("user_choice_update")
245 {
247  const int max_side = static_cast<int>(resources::gameboard->teams().size());
248 
249  for(int side : required_)
250  {
251  assert(1 <= side && side <= max_side);
252  const team& t = resources::gameboard->get_team(side);
253  assert(!t.is_empty());
254  if(side != current_side_)
255  {
257  }
258  }
259 
262 
263 }
264 
266 {
267  while(!finished() && !oos_)
268  {
270  if(resources::recorder->at_end()) {
271  return;
272  }
273 
274  DBG_REPLAY << "MP synchronization: extracting choice from replay with has_local_side=" << has_local_choice();
275 
276  const config *action = resources::recorder->get_next_action();
277  assert(action); //action cannot be null because resources::recorder->at_end() returned false.
278  if( !action->has_child(tagname_) || !(*action)["dependent"].to_bool())
279  {
280  replay::process_error("[" + tagname_ + "] expected but none found\n. found instead:\n" + action->debug());
281  //We save this action for later
283  // execute this local choice locally
284  oos_ = true;
286  return;
287  }
288  int from_side = (*action)["from_side"].to_int(0);
289  if((*action)["side_invalid"].to_bool(false) == true)
290  {
291  //since this 'cheat' can have a quite heavy effect especially in umc content we give an oos error .
292  replay::process_error("MP synchronization: side_invalid in replay data, this could mean someone wants to cheat.\n");
293  }
294  if(required_.find(from_side) == required_.end())
295  {
296  replay::process_error("MP synchronization: we got an answer from side " + std::to_string(from_side) + "for [" + tagname_ + "] which is not was we expected\n");
297  }
298  if(res_.find(from_side) != res_.end())
299  {
300  replay::process_error("MP synchronization: we got already our answer from side " + std::to_string(from_side) + "for [" + tagname_ + "] now we have it twice.\n");
301  }
302  res_[from_side] = action->mandatory_child(tagname_);
304  }
305 }
307 {
308  // there might be speak or similar commands in the replay before the user input.
314 }
315 
317 {
318  int local_choice_prev = local_choice_;
319  //equals to any side in sides that is local, 0 if no such side exists.
320  local_choice_ = 0;
321  //if for any side from which we need an answer
322  std::vector<t_string> sides_str;
323  for(int side : required_)
324  {
325  //and we haven't already received our answer from that side
326  if(res_.find(side) == res_.end())
327  {
328  sides_str.push_back(std::to_string(side));
329  //and it is local
330  if(resources::gameboard->get_team(side).is_local() && !resources::gameboard->get_team(side).is_idle())
331  {
332  //then we have to make a local choice.
333  local_choice_ = side;
334  break;
335  }
336  }
337  }
338 
339  // TRANSLATORS: In networked games, this text is shown on the map while
340  // waiting for $desc from another player.
341  // Don't end the text with a punctuation sign.
343  "waiting for $desc from side $sides",
344  "waiting for $desc from sides $sides",
345  sides_str.size(),
346  {std::pair("desc", uch_.description()), std::pair("sides", utils::format_conjunct_list("", sides_str))}
347  );
348  if(local_choice_prev != local_choice_) {
350  }
351 }
352 
354 {
355  assert(local_choice_ != 0);
356 
358  /* At least one of the decisions is ours, and it will be inserted
359  into the replay. */
360  DBG_REPLAY << "MP synchronization: local choice";
362  if(res_.find(local_choice_) != res_.end()) {
363  // It might be possible that we this choice was already made by another client while we were in uch_.query_user
364  // because our side might be reassigned while we made our choice.
365  WRN_REPLAY << "Discarding a local choice because we found it already on the replay";
366  return;
367  }
369  res_[local_choice_] = cfg;
370 
371  //send data to others.
372  //but if there wasn't any data sent during this turn, we don't want to begin with that now.
373  //TODO: we should send user choices during nonundoable actions immediately.
375  {
377  }
379 }
380 
382 {
383  assert(oos_);
384  ERR_REPLAY << "A sync error appeared while waiting for a synced user choice of type '" << uch_.description() << "' ([" + tagname_ + "]), doing the choice locally";
385  for(int side : required_)
386  {
387  if(res_.find(side) == res_.end())
388  {
389  ERR_REPLAY << "Doing a local choice for side " << side;
390  res_[side] = uch_.query_user(side);
391  }
392  }
393  oos_ = false;
394 }
395 
397 {
398  user_choice_notifer_ingame notifer;
399  while(!man.finished() && man.waiting())
400  {
401  //TODO: didn't we already check for this?
402  if(!resources::gamedata->is_before_screen())
403  {
404  //during the prestart/preload event the screen is locked and we shouldn't call user_interact.
405  //because that might result in crashes if someone clicks anywhere during screenlock.
406 
407  // calls man.pull via events.cpp -> pump_monitor::process
409  }
410 
411  notifer.update(man.wait_message());
412  }
413 }
414 
416 {
417  gui2::dialogs::synched_choice_wait::display(man);
418 }
419 
420 std::map<int, config> user_choice_manager::get_user_choice_internal(const std::string &name, const mp_sync::user_choice &uch, const std::set<int>& sides)
421 {
422  const bool is_too_early = resources::gamedata->is_before_screen();
423  user_choice_manager man(name, uch, sides);
424  while(!man.finished())
425  {
426  if(man.waiting())
427  {
428  if(is_too_early) {
429  wait_prestart(man);
430  }
431  else {
432  wait_ingame(man);
433  }
434  }
435  else if(man.has_local_choice())
436  {
437  man.ask_local_choice();
438  }
439  else
440  {
441  man.fix_oos();
442  }
443  }
444  return man.res_;
445 }
446 
447 namespace {
448  // we want to prevent calling pull() while we are already calling pull()
449  // this could for example happen if pull() receives a [side_drop] and
450  // user_choice_manager::process is called while the "player has left the game.
451  // What do you want to do?" dialog is shown.
452  static bool ucm_in_proccess = false;
453  struct ucm_process_scope {
454  ucm_process_scope() { ucm_in_proccess = true; }
455  ~ucm_process_scope() { ucm_in_proccess = false; }
456  };
457 }
459 {
460  if(!oos_ && !finished() && !ucm_in_proccess)
461  {
462  ucm_process_scope scope1;
463  pull();
464  }
465 }
int current_side_
Definition: move.cpp:320
double t
Definition: astarsearch.cpp:63
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(bool is_delay_enabled=true)
rect map_outside_area() const
Returns the available area for a map, this may differ from the above.
Definition: display.cpp:520
static display * get_singleton()
Returns the display object if a display object exists.
Definition: display.hpp:111
virtual void notify_observers()
void set_position(double xpos, double ypos)
void set_lifetime(const std::chrono::milliseconds &lifetime, const std::chrono::milliseconds &fadeout=std::chrono::milliseconds{100})
void set_color(const color_t &color)
void set_clip_rect(const SDL_Rect &r)
void set_font_size(int font_size)
virtual const std::vector< team > & teams() const override
Definition: game_board.hpp:80
team & get_team(int i)
Definition: game_board.hpp:92
bool is_before_screen() const
Definition: game_data.cpp:197
A RAII object to temporary leave the synced context like in wesnoth.synchronize_choice.
int current_side() const
Returns the number of the side whose turn it is.
virtual bool is_networked_mp() const
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition: replay.cpp:259
void revert_action()
Definition: replay.cpp:615
static void process_error(const std::string &msg)
Definition: replay.cpp:200
config * get_next_action()
Definition: replay.cpp:622
static bool undo_blocked()
static void pull_remote_user_input()
called from get_user_choice while waiting for a remove user choice.
static bool is_synced()
static void send_user_choice()
called from get_user_choice to send a recently made choice to the other clients.
static void set_is_simultaneous()
Sets is_simultaneous_ = true, called using a user choice that is not the currently playing side.
This class stores all the data for a single 'side' (in game nomenclature).
Definition: team.hpp:75
bool is_empty() const
Definition: team.hpp:245
events::generic_event changed_event_
bool waiting() const
Note: currently finished() does not imply !waiting() so you may need to check both.
user_choice_manager(const std::string &name, const mp_sync::user_choice &uch, const std::set< int > &sides)
std::set< int > required_
const mp_sync::user_choice & uch_
void process()
Inherited from events::pump_monitor.
static std::map< int, config > get_user_choice_internal(const std::string &name, const mp_sync::user_choice &uch, const std::set< int > &sides)
std::map< int, config > res_
const std::string & wait_message() const
const std::string & tagname_
map_display and display: classes which take care of displaying the map and game-data on the screen.
#define VNGETTEXT(msgid, msgid_plural, count,...)
Standard logging facilities (interface).
static void update()
const int SIZE_LARGE
Definition: constants.cpp:30
int add_floating_label(const floating_label &flabel)
add a label floating on the screen above everything else.
void remove_floating_label(int handle, const std::chrono::milliseconds &fadeout)
removes the floating label given by 'handle' from the screen
const color_t NORMAL_COLOR
config get_user_choice(const std::string &name, const user_choice &uch, int side=0)
std::map< int, config > get_user_choice_multiple_sides(const std::string &name, const user_choice &uch, std::set< int > sides)
Performs a choice for multiple sides for WML events.
game_board * gameboard
Definition: resources.cpp:20
game_data * gamedata
Definition: resources.cpp:22
replay * recorder
Definition: resources.cpp:28
play_controller * controller
Definition: resources.cpp:21
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:729
Replay control code.
Interface for querying local choices.
virtual config query_user(int side) const =0
virtual std::string description() const
virtual bool is_visible() const
whether the choice is visible for the user like an advancement choice a non-visible choice is for exa...
virtual config random_choice(int side) const =0
static void wait_ingame(user_choice_manager &man)
static void wait_prestart(user_choice_manager &man)
#define WRN_REPLAY
#define DBG_REPLAY
static lg::log_domain log_replay("replay")
#define LOG_REPLAY
#define ERR_REPLAY