The Battle for Wesnoth  1.13.4+dev
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
synced_user_choice.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2015 - 2016 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 "config_assign.hpp"
19 #include "floating_label.hpp"
20 #include "game_display.hpp"
21 #include "game_data.hpp"
22 #include "log.hpp"
23 #include "play_controller.hpp"
24 #include "synced_context.hpp"
25 #include "replay.hpp"
26 #include "resources.hpp"
28 #include <boost/lexical_cast.hpp>
29 #include <set>
30 #include <map>
31 #include "formula/string_utils.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  class user_choice_notifer_ingame
42  {
43  //the handle for the label on the screen -1 if not shown yet.
44  int label_id_;
45  std::string message_;
46  unsigned int start_show_;
47 
48  public:
49  user_choice_notifer_ingame()
50  : label_id_(-1)
51  , message_()
52  , start_show_(SDL_GetTicks() + 2000)
53  {
54 
55  }
56 
57  ~user_choice_notifer_ingame()
58  {
59  if(label_id_ != -1) {
60  end_show_label();
61  }
62  }
63 
64  void update(const std::string& message)
65  {
66  if(label_id_ == -1 && SDL_GetTicks() > start_show_)
67  {
68  start_show_label();
69  }
70  if(message == message_) {
71  return;
72  }
73  message_ = message;
74  if(label_id_ != -1) {
75  end_show_label();
76  start_show_label();
77  }
78  }
79 
80  void start_show_label()
81  {
82  assert(label_id_ == -1);
83  SDL_Rect area = resources::screen->map_outside_area();
84  font::floating_label flabel(message_);
87  flabel.set_position(area.w/2, area.h/4);
88  flabel.set_lifetime(-1);
89  flabel.set_clip_rect(area);
90  label_id_ = font::add_floating_label(flabel);
91  }
92 
93  void end_show_label()
94  {
95  assert(label_id_ != -1);
96  font::remove_floating_label(label_id_);
97  label_id_ = -1;
98  }
99  };
100 }
101 
103  std::set<int> sides)
104 {
105  //pass sides by copy because we need a copy.
106  const bool is_synced = synced_context::is_synced();
107  const int max_side = static_cast<int>(resources::teams->size());
108  //we currently don't check for too early because luas sync choice doesn't necessarily show screen dialogs.
109  //It (currently) in the responsibility of the user of sync choice to not use dialogs during prestart events..
110  if(!is_synced)
111  {
112  //we got called from inside luas wesnoth.synchronize_choice or from a select event.
113  replay::process_error("MP synchronization only works in a synced context (for example Select or preload events are no synced context).\n");
114  return std::map<int,config>();
115  }
116 
117  /*
118  for empty sides we want to use random choice instead.
119  */
120  std::set<int> empty_sides;
121  for(int side : sides)
122  {
123  assert(1 <= side && side <= max_side);
124  if( (*resources::teams)[side-1].is_empty())
125  {
126  empty_sides.insert(side);
127  }
128  }
129 
130  for(int side : empty_sides)
131  {
132  sides.erase(side);
133  }
134 
135  std::map<int,config> retv = user_choice_manager::get_user_choice_internal(name, uch, sides);
136 
137  for(int side : empty_sides)
138  {
139  retv[side] = uch.random_choice(side);
140  }
141  return retv;
142 
143 }
144 
145 /*
146  fixes some rare cases and calls get_user_choice_internal if we are in a synced context.
147 */
149  int side)
150 {
151  const bool is_too_early = resources::gamedata->phase() != game_data::START && resources::gamedata->phase() != game_data::PLAY;
152  const bool is_synced = synced_context::is_synced();
153  const bool is_mp_game = resources::controller->is_networked_mp();//Only used in debugging output below
154  const int max_side = static_cast<int>(resources::teams->size());
155  bool is_side_null_controlled;
156 
157  if(!is_synced)
158  {
159  //we got called from inside luas wesnoth.synchronize_choice or from a select event (or maybe a preload event?).
160  //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,
161  //so just give a warning.
162  LOG_REPLAY << "MP synchronization called during an unsynced context.\n";
163  return uch.query_user(side);
164  }
165  if(is_too_early && uch.is_visible())
166  {
167  //We are in a prestart event or even earlier.
168  //Although we are able to sync them, we cannot use query_user,
169  //because we cannot (or shouldn't) put things on the screen inside a prestart event, this is true for SP and MP games.
170  //Quotation form event wiki: "For things displayed on-screen such as character dialog, use start instead"
171  return uch.random_choice(side);
172  }
173  //in start events it's unclear to decide on which side the function should be executed (default= side1 still).
174  //But for advancements we can just decide on the side that owns the unit and that's in the responsibility of advance_unit_at.
175  //For [message][option] and luas sync_choice the scenario designer is responsible for that.
176  //For [get_global_variable] side is never null.
177 
178  /*
179  side = 0 should default to the currently active side per definition.
180  */
181  if ( side < 1 || max_side < side )
182  {
183  if(side != 0)
184  {
185  ERR_REPLAY << "Invalid parameter for side in get_user_choice." << std::endl;
186  }
188  LOG_REPLAY << " side changed to " << side << "\n";
189  }
190  is_side_null_controlled = (*resources::teams)[side-1].is_empty();
191 
192  LOG_REPLAY << "get_user_choice_called with"
193  << " name=" << name
194  << " is_synced=" << is_synced
195  << " is_mp_game=" << is_mp_game
196  << " is_side_null_controlled=" << is_side_null_controlled << "\n";
197 
198  if (is_side_null_controlled)
199  {
200  DBG_REPLAY << "MP synchronization: side 1 being null-controlled in get_user_choice.\n";
201  //most likely we are in a start event with an empty side 1
202  //but calling [set_global_variable] to an empty side might also cause this.
203  //i think in that case we should better use uch.random_choice(),
204  //which could return something like config_of("invalid", true);
205  side = 1;
206  while ( side <= max_side && (*resources::teams)[side-1].is_empty() )
207  side++;
208  assert(side <= max_side);
209  }
210 
211 
212  assert(1 <= side && side <= max_side);
213 
214  std::set<int> sides;
215  sides.insert(side);
216  std::map<int, config> retv = user_choice_manager::get_user_choice_internal(name, uch, sides);
217  if(retv.find(side) == retv.end())
218  {
219  //An error occured, get_user_choice_internal should have given an oos error message
220  return config();
221  }
222  return retv[side];
223 }
224 
226  : required_(sides)
227  , res_()
228  , local_choice_(0)
229  , wait_message_()
230  , oos_(false)
231  , uch_(uch)
232  , tagname_(name)
233  , current_side_(resources::controller->current_side())
234  , changed_event_("user_choice_update")
235 {
237  const int max_side = static_cast<int>(resources::teams->size());
238 
239  for(int side : required_)
240  {
241  assert(1 <= side && side <= max_side);
242  const team& t = (*resources::teams)[side-1];
243  assert(!t.is_empty());
244  if(side != current_side_)
245  {
247  }
248  }
249 
252 
253 }
254 
256 {
257  while(!finished() && !oos_)
258  {
260  if(resources::recorder->at_end()) {
261  return;
262  }
263 
264  DBG_REPLAY << "MP synchronization: extracting choice from replay with has_local_side=" << has_local_choice() << "\n";
265 
266  const config *action = resources::recorder->get_next_action();
267  assert(action); //action cannot be null because resources::recorder->at_end() returned false.
268  if( !action->has_child(tagname_) || !(*action)["dependent"].to_bool())
269  {
270  replay::process_error("[" + tagname_ + "] expected but none found\n. found instead:\n" + action->debug());
271  //We save this action for later
273  // execute this local choice locally
274  oos_ = true;
276  return;
277  }
278  int from_side = (*action)["from_side"].to_int(0);
279  if((*action)["side_invalid"].to_bool(false) == true)
280  {
281  //since this 'cheat' can have a quite heavy effect especialy in umc content we give an oos error .
282  replay::process_error("MP synchronization: side_invalid in replay data, this could mean someone wants to cheat.\n");
283  }
284  if(required_.find(from_side) == required_.end())
285  {
286  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");
287  }
288  if(res_.find(from_side) != res_.end())
289  {
290  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");
291  }
292  res_[from_side] = action->child(tagname_);
294  }
295 }
297 {
298  // there might be speak or similar commands in the replay before the user input.
304 }
305 
307 {
308  int local_choice_prev = local_choice_;
309  //equals to any side in sides that is local, 0 if no such side exists.
310  local_choice_ = 0;
311  //if for any side from which we need an answer
312  std::string sides_str;
313  for(int side : required_)
314  {
315  //and we havent already received our answer from that side
316  if(res_.find(side) == res_.end())
317  {
318  sides_str += " ";
319  sides_str += std::to_string(side);
320  //and it is local
321  if((*resources::teams)[side-1].is_local() && !(*resources::teams)[side-1].is_idle())
322  {
323  //then we have to make a local choice.
324  local_choice_ = side;
325  break;
326  }
327  }
328  }
329  wait_message_ = vgettext("waiting for $desc from side(s)$sides", string_map_of("desc", uch_.description())("sides", sides_str));
330  if(local_choice_prev != local_choice_) {
332  }
333 }
334 
336 {
337  assert(local_choice_ != 0);
338 
340  /* At least one of the decisions is ours, and it will be inserted
341  into the replay. */
342  DBG_REPLAY << "MP synchronization: local choice\n";
344  if(res_.find(local_choice_) != res_.end()) {
345  // It might be possible that we this choice was already made by another client while we were in uch_.query_user
346  // becase our side might be ressigned while we made our choice.
347  WRN_REPLAY << "Discarding a local choice becasue we found it already on the replay";
348  return;
349  }
351  res_[local_choice_] = cfg;
352 
353  //send data to others.
354  //but if there wasn't any data sended during this turn, we don't want to bein wth that now.
355  //TODO: we should send user choices during nonundoable actions immideatley.
357  {
359  }
361 }
362 
364 {
365  assert(oos_);
366  ERR_REPLAY << "A sync error appeared while waiting for a synced user choice of type '" << uch_.description() << "' ([" + tagname_ + "]), doing the choice locally\n";
367  for(int side : required_)
368  {
369  if(res_.find(side) == res_.end())
370  {
371  ERR_REPLAY << "Doing a local choice for side " << side << "\n";
372  res_[side] = uch_.query_user(side);
373  }
374  }
375  oos_ = false;
376 }
377 
379 {
380  user_choice_notifer_ingame notifer;
381  while(!man.finished() && man.waiting())
382  {
384  {
385  //during the prestart/preload event the screen is locked and we shouldn't call user_interact.
386  //because that might result in crashs if someone clicks anywhere during screenlock.
387 
388  // calls man.pull via events.cpp -> pump_monitor::process
390  }
391 
392  notifer.update(man.wait_message());
393  }
394 }
395 
397 {
398  gui2::tsynced_choice_wait scw(man);
399  scw.show(resources::screen->video());
400 }
401 
402 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)
403 {
404  const bool is_too_early = resources::gamedata->phase() != game_data::START && resources::gamedata->phase() != game_data::PLAY;
405  user_choice_manager man(name, uch, sides);
406  while(!man.finished())
407  {
408  if(man.waiting())
409  {
410  if(is_too_early) {
411  wait_prestart(man);
412  }
413  else {
414  wait_ingame(man);
415  }
416  }
417  else if(man.has_local_choice())
418  {
419  man.ask_local_choice();
420  }
421  else
422  {
423  man.fix_oos();
424  }
425  }
426  return man.res_;
427 }
428 
430 {
431  if(waiting())
432  {
433  pull();
434  }
435 }
436 
437 
play_controller * controller
Definition: resources.cpp:21
virtual std::string description() const
config get_user_choice(const std::string &name, const user_choice &uch, int side=0)
#define DBG_REPLAY
#define LOG_REPLAY
user_choice_manager(const std::string &name, const mp_sync::user_choice &uch, std::set< int > sides)
PHASE phase() const
Definition: game_data.hpp:78
static lg::log_domain log_replay("replay")
virtual void notify_observers()
void set_clip_rect(const SDL_Rect &r)
std::set< int > required_
const std::string & tagname_
bool show(CVideo &video, const unsigned auto_close_time=0)
Shows the window.
Definition: dialog.cpp:34
game_display * screen
Definition: resources.cpp:27
void remove_floating_label(int handle)
removes the floating label given by 'handle' from the screen
static void wait_ingame(user_choice_manager &man)
void play_slice(bool is_delay_enabled=true)
Replay control code.
void set_font_size(int font_size)
std::string debug() const
Definition: config.cpp:1438
const SDL_Color NORMAL_COLOR
Definition: font.cpp:564
GLdouble GLdouble t
Definition: glew.h:1366
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 mutiple sides for WML events.
virtual config random_choice(int side) const =0
game_data * gamedata
Definition: resources.cpp:22
boost::assign_detail::generic_list< std::pair< boost::assign_detail::assign_decay< std::string >::type, boost::assign_detail::assign_decay< std::string >::type > > string_map_of(const std::string &k, const std::string &v)
An alias for boost::assign::map_list_of
const int SIZE_XLARGE
Definition: font.hpp:68
bool waiting()
Note: currently finished() does not imply !waiting() so you may need to check both.
bool has_child(const std::string &key) const
Determine whether a config has a child or not.
Definition: config.cpp:651
This class stores all the data for a single 'side' (in game nomenclature).
Definition: team.hpp:50
int current_side() const
Returns the number of the side whose turn it is.
std::vector< team > * teams
Definition: resources.cpp:29
void set_lifetime(int lifetime)
virtual bool is_visible() const
whether the choice is visible for the user like an advacement choice a non-visible choice is for exam...
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_color(const SDL_Color &color)
void set_position(double xpos, double ypos)
const std::string & wait_message()
replay * recorder
Definition: resources.cpp:30
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
static bool is_simultaneously()
static void process_error(const std::string &msg)
Definition: replay.cpp:198
int current_side_
Definition: move.cpp:308
int add_floating_label(const floating_label &flabel)
add a label floating on the screen above everything else.
std::string vgettext(const char *msgid, const utils::string_map &symbols)
GLuint const GLchar * name
Definition: glew.h:1782
#define WRN_REPLAY
bool is_empty() const
Definition: team.hpp:259
config & child(const std::string &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:658
Various functions that implement the undoing (and redoing) of in-game commands.
Standard logging facilities (interface).
const SDL_Rect & map_outside_area() const
Returns the available area for a map, this may differ from the above.
Definition: display.hpp:248
config * get_next_action()
Definition: replay.cpp:598
GLsizei GLenum GLuint GLuint GLsizei char * message
Definition: glew.h:2499
void revert_action()
Definition: replay.cpp:591
std::map< int, config > res_
virtual bool is_networked_mp() const
void user_input(const std::string &, const config &, int from_side)
adds a user_input to the replay
Definition: replay.cpp:251
static void set_is_simultaneously()
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:83
REPLAY_RETURN do_replay_handle(bool one_move)
Definition: replay.cpp:677
static bool is_synced()
GLsizei const GLcharARB ** string
Definition: glew.h:4503
static void wait_prestart(user_choice_manager &man)
#define ERR_REPLAY