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