The Battle for Wesnoth  1.19.0-dev
playmp_controller.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2006 - 2024
3  by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
4  Copyright (C) 2003 by David White <dave@whitevine.net>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 #include "playmp_controller.hpp"
18 
19 #include "actions/undo.hpp"
20 #include "countdown_clock.hpp"
21 #include "display_chat_manager.hpp"
22 #include "floating_label.hpp"
23 #include "game_end_exceptions.hpp"
25 #include "gettext.hpp"
28 #include "log.hpp"
29 #include "mp_ui_alerts.hpp"
30 #include "playturn.hpp"
31 #include "preferences/game.hpp"
32 #include "preferences/general.hpp"
33 #include "replay_helper.hpp"
34 #include "resources.hpp"
35 #include "savegame.hpp"
37 #include "synced_context.hpp"
38 #include "video.hpp" // only for faked
39 #include "wesnothd_connection.hpp"
40 
41 static lg::log_domain log_engine("engine");
42 #define LOG_NG LOG_STREAM(info, log_engine)
43 #define DBG_NG LOG_STREAM(debug, log_engine)
44 
46  : playsingle_controller(level, state_of_game, mp_info && mp_info->skip_replay)
47  , network_processing_stopped_(false)
48  , blindfold_(*gui_, mp_info && mp_info->skip_replay_blindfolded)
49  , mp_info_(mp_info)
50 {
51  // upgrade hotkey handler to the mp (network enabled) version
52  hotkey_handler_.reset(new hotkey_handler(*this, saved_game_));
53 
54  // turn_data_.set_host(is_host);
56  if(!mp_info || mp_info->current_turn <= turn()) {
57  skip_replay_ = false;
58  }
59 
60  if(gui_->is_blindfolded() && !is_observer()) {
62  }
63 }
64 
66 {
67  // halt and cancel the countdown timer
68  try {
70  } catch(...) {
71  DBG_NG << "Caught exception in playmp_controller destructor: " << utils::get_unknown_exception_type();
72  }
73 }
74 
76 {
78  LOG_NG << "network processing activated again";
79 }
80 
82 {
84  LOG_NG << "network processing stopped";
85 }
86 
88 {
90 }
91 
93 {
94  if(gui_->is_blindfolded()) {
96  LOG_NG << "Taking off the blindfold now";
97  gui_->queue_rerender();
98  }
99 }
100 
102 {
104  if(replay_controller_.get() != nullptr) {
105  // We have probably been using the mp "back to turn" feature
106  // We continue play since we have reached the end of the replay.
107  replay_controller_.reset();
108  }
109 
110  while( gamestate().in_phase(game_data::GAME_ENDED) && !end_turn_requested_) {
111  config cfg;
112  if(network_reader_.read(cfg)) {
114  end_turn();
115  }
116  }
117 
118  play_slice();
119  }
120 }
121 
123 {
124  LOG_NG << "playmp::play_human_turn...";
125  assert(gamestate().in_phase(game_data::TURN_PLAYING));
126 
127  mp::ui_alerts::turn_changed(current_team().current_player());
128 
129  LOG_NG << "events::commands_disabled=" << events::commands_disabled;
130 
132 
133  const std::unique_ptr<countdown_clock> timer(saved_game_.mp_settings().mp_countdown
135  : nullptr);
136 
138 
139  if(undo_stack().can_undo()) {
140  // If we reload a networked mp game we cannot undo moves made before the save
141  // because other players already received them
142  if(!current_team().auto_shroud_updates()) {
144  }
145  undo_stack().clear();
146  }
147 
149  execute_gotos();
150  }
151 
152  end_turn_enable(true);
153 
154  while(!should_return_to_play_side()) {
155  try {
160  // Clean undo stack if turn has to be restarted (losing control)
161  if(undo_stack().can_undo()) {
162  font::floating_label flabel(_("Undoing moves not yet transmitted to the server."));
163 
164  color_t color{255, 255, 255, SDL_ALPHA_OPAQUE};
165  flabel.set_color(color);
166  SDL_Rect rect = gui_->map_area();
167  flabel.set_position(rect.w / 2, rect.h / 2);
168  flabel.set_lifetime(2500);
169  flabel.set_clip_rect(rect);
170 
171  font::add_floating_label(flabel);
172  }
173 
174  while(undo_stack().can_undo()) {
175  undo_stack().undo();
176  }
177  }
178 
179  if(timer) {
180  bool time_left = timer->update();
181  if(!time_left) {
182  end_turn_requested_ = true;
183  }
184  }
185  } catch(...) {
186  DBG_NG << "Caught exception while playing a side: " << utils::get_unknown_exception_type();
187  throw;
188  }
189 
191  }
192 }
193 
195 {
196  LOG_NG << "playmp::play_human_turn...";
197 
199 
200  while(!should_return_to_play_side()) {
201  try {
204  SDL_Delay(1);
205  } catch(...) {
206  DBG_NG << "Caught exception while playing idle loop: " << utils::get_unknown_exception_type();
207  throw;
208  }
209 
211  }
212 }
213 
215 {
216  LOG_NG << "beginning end-of-scenario linger";
217 
218  // End all unit moves
220 
222 
223  assert(is_regular_game_end());
224 
225  if(get_end_level_data().transient.reveal_map) {
226  // Change the view of all players and observers
227  // to see the whole map regardless of shroud and fog.
228  update_gui_to_player(gui_->viewing_team(), true);
229  }
230 
231  bool quit;
232  do {
233  quit = true;
234  try {
235  // reimplement parts of play_side()
239  LOG_NG << "finished human turn";
240  } catch(const savegame::load_game_exception&) {
241  LOG_NG << "caught load-game-exception";
242  // this should not happen, the option to load a game is disabled
243  throw;
244  } catch(const leavegame_wesnothd_error& e) {
245  scoped_savegame_snapshot snapshot(*this);
247 
248  if(e.message == "") {
249  save.save_game_interactive(_("A network disconnection has occurred, and the game cannot continue. Do you want to save the game?"),
251  } else {
253  _("This game has been ended.\nReason: ") + e.message + _("\nDo you want to save the game?"),
255  }
256  throw;
257  } catch(const ingame_wesnothd_error&) {
258  LOG_NG << "caught network-error-exception";
259  quit = false;
260  }
261  } while(!quit);
262 
263  LOG_NG << "ending end-of-scenario linger";
264 }
265 
267 {
269  // If the host is here we'll never leave since we wait for the host to
270  // upload the next scenario.
271  assert(!is_host());
272  // TODO: should we handle the case that we become the ho0st because the host disconnectes here?a
273 
276  while(true) {
278  if(res == turn_info::PROCESS_END_LINGER) {
279  return;
280  } else if (res != turn_info::PROCESS_CONTINUE) {
281  throw quit_game_exception();
282  }
283  SDL_Delay(10);
285  }
286  });
287 }
288 
290 {
292  // time_left + turn_bonus + (action_bonus * number of actions done)
293  const int new_time_in_secs = (current_team().countdown_time() / 1000)
296 
297  const int new_time
298  = 1000 * std::min<int>(new_time_in_secs, saved_game_.mp_settings().mp_countdown_reservoir_time);
299 
301  current_team().set_countdown_time(new_time);
302 
304  }
305 
306  LOG_NG << "playmp::after_human_turn...";
307 
308  // Normal post-processing for human turns (clear undos, end the turn, etc.)
310 
311  // send one more time to make sure network is up-to-date.
313 }
314 
316 {
317  LOG_NG << "is networked...";
318 
319  end_turn_enable(false);
321 
325  if(!mp_info_ || mp_info_->current_turn == turn()) {
326  skip_replay_ = false;
327  }
328  }
329 
333  }
334  }
335 
336  LOG_NG << "finished networked...";
337 }
338 
339 void playmp_controller::process_oos(const std::string& err_msg) const
340 {
341  // Notify the server of the oos error.
342  config cfg;
343  config& info = cfg.add_child("info");
344  info["type"] = "termination";
345  info["condition"] = "out of sync";
346  send_to_wesnothd(cfg);
347 
348  std::stringstream temp_buf;
349  std::vector<std::string> err_lines = utils::split(err_msg, '\n');
350  temp_buf << _("The game is out of sync, and cannot continue. There are a number of reasons this could happen: this "
351  "can occur if you or another player have modified their game settings. This may mean one of the "
352  "players is attempting to cheat. It could also be due to a bug in the game, but this is less "
353  "likely.\n\nDo you want to save an error log of your game?");
354 
355  if(!err_msg.empty()) {
356  temp_buf << " \n \n"; // and now the "Details:"
357  for(std::vector<std::string>::iterator i = err_lines.begin(); i != err_lines.end(); ++i) {
358  temp_buf << *i << '\n';
359  }
360  temp_buf << " \n";
361  }
362 
363  scoped_savegame_snapshot snapshot(*this);
365 
367 }
368 
369 void playmp_controller::handle_generic_event(const std::string& name)
370 {
371  if(name == "ai_user_interact") {
374  } else if(name == "ai_gamestate_changed") {
376  } else if(name == "host_transfer") {
377  assert(mp_info_);
378  mp_info_->is_host = true;
379  if(is_linger_mode()) {
380  end_turn_enable(true);
381  }
382  }
383 }
385 {
386  return !mp_info_ || mp_info_->is_host;
387 }
388 
390 {
391  gui_->get_chat_manager().add_chat_message(std::time(nullptr), "", 0,
392  _("This side is in an idle state. To proceed with the game, it must be assigned to another controller. You may "
393  "use :droid, :control or :give_control for example."),
395 }
396 
398 {
399  // mouse_handler expects at least one team for linger mode to work.
400  assert(is_regular_game_end());
401  if(!get_end_level_data().transient.linger_mode || get_teams().empty() || video::headless()) {
402  const bool has_next_scenario
403  = !gamestate().gamedata_.next_scenario().empty() && gamestate().gamedata_.next_scenario() != "null";
404  if(!is_host() && has_next_scenario) {
405  // If we continue without lingering we need to
406  // make sure the host uploads the next scenario
407  // before we attempt to download it.
408  wait_for_upload();
409  }
410  } else {
411  linger();
412  }
413  end_turn_requested_ = true;
414 }
415 
417 {
418  undo_stack().clear();
421 }
422 
424 {
426  assert(res != turn_info::PROCESS_END_TURN);
427 
428  if(res == turn_info::PROCESS_END_LINGER) {
429  // Probably some bad OOS, but there is currently no way to recover from this.
430  throw ingame_wesnothd_error("");
431  }
432 
434  player_type_changed_ = true;
435  }
436 }
437 
439 {
441 }
442 
443 void playmp_controller::play_slice(bool is_delay_enabled)
444 {
446  // receive chat during animations and delay
447  process_network_data(true);
448  // cannot use turn_data_.send_data() here.
449  // todo: why? The checks in turn_data_.send_data() should be safe enouth.
451  }
452 
453  playsingle_controller::play_slice(is_delay_enabled);
454 }
455 
457 {
459  return;
460  }
461 
463  config cfg;
464 
465  if(!resources::recorder->at_end()) {
467  } else if(network_reader_.read(cfg)) {
468  res = turn_data_.process_network_data(cfg, chat_only);
469  }
470 
472  network_reader_.push_front(std::move(cfg));
473  } else if(res == turn_info::PROCESS_RESTART_TURN) {
474  player_type_changed_ = true;
475  } else if(res == turn_info::PROCESS_END_TURN) {
476  } else if(res == turn_info::PROCESS_END_LEVEL) {
477  } else if(res == turn_info::PROCESS_END_LINGER) {
478  replay::process_error("Received unexpected next_scenario during the game");
479  }
480 }
481 
483 {
484  return mp_info_ != nullptr;
485 }
486 
487 void playmp_controller::send_to_wesnothd(const config& cfg, const std::string&) const
488 {
489  if(mp_info_ != nullptr) {
491  }
492 }
493 
495 {
496  if(mp_info_ != nullptr) {
497  return mp_info_->connection.receive_data(cfg);
498  } else {
499  return false;
500  }
501 }
void clear()
Clears the stack of undoable (and redoable) actions.
Definition: undo.cpp:201
void undo()
Undoes the top action on the undo stack.
Definition: undo.cpp:330
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
config & add_child(config_key_type key)
Definition: config.cpp:441
virtual void play_slice(bool is_delay_enabled=true)
virtual bool attach_handler(observer *obs)
virtual bool detach_handler(observer *obs)
void set_lifetime(int lifetime, int fadeout=100)
void set_position(double xpos, double ypos)
void set_color(const color_t &color)
void set_clip_rect(const SDL_Rect &r)
void set_all_units_user_end_turn()
Definition: game_board.cpp:88
@ GAME_ENDED
The game has ended and the user is observing the final state "lingering" The game can be saved here.
Definition: game_data.hpp:102
@ TURN_PLAYING
The User is controlling the game and invoking actions The game can be saved here.
Definition: game_data.hpp:93
@ TURN_ENDED
The turn_end, side_turn_end etc [events] are fired next phase: TURN_STARTING_WAITING (default),...
Definition: game_data.hpp:96
const std::string & next_scenario() const
Definition: game_data.hpp:130
game_board board_
Definition: game_state.hpp:44
game_data gamedata_
Definition: game_state.hpp:43
static void progress(loading_stage stage=loading_stage::none)
Report what is being loaded to the loading screen.
static void display(std::function< void()> f)
static void spin()
Indicate to the player that loading is progressing.
std::vector< team > & get_teams()
std::unique_ptr< hotkey_handler > hotkey_handler_
bool is_linger_mode() const
actions::undo_list & undo_stack()
bool is_observer() const
bool is_regular_game_end() const
saved_game & saved_game_
game_state & gamestate()
std::unique_ptr< game_display > gui_
const end_level_data & get_end_level_data() const
int current_side() const
Returns the number of the side whose turn it is.
bool is_replay() const
bool player_type_changed_
true when the controller of the currently playing side has changed.
std::size_t turn() const
void update_gui_to_player(const int team_index, const bool observe=false)
Changes the UI for this client to the passed side index.
bool can_undo() const
void process_network_data(bool chat_only=false)
void process_oos(const std::string &err_msg) const override
Asks the user whether to continue on an OOS error.
mp_game_metadata * mp_info_
virtual bool is_host() const override
virtual void do_idle_notification() override
Will handle sending a networked notification in descendent classes.
void maybe_linger() override
bool receive_from_wesnothd(config &cfg) const override
virtual void play_idle_loop() override
virtual void handle_generic_event(const std::string &name) override
virtual void play_human_turn() override
void send_to_wesnothd(const config &cfg, const std::string &packet_type="unknown") const override
virtual void play_linger_turn()
void send_user_choice() override
void pull_remote_choice() override
virtual void on_not_observer() override
virtual void after_human_turn() override
bool is_networked_mp() const override
virtual void play_network_turn() override
Will handle networked turns in descendent classes.
void surrender(int side_number)
void wait_for_upload()
Wait for the host to upload the next scenario.
playmp_controller(const config &level, saved_game &state_of_game, mp_game_metadata *mp_info)
void play_slice(bool is_delay_enabled=true) override
void end_turn_enable(bool enable)
std::unique_ptr< replay_controller > replay_controller_
non-null when replay mode in active, is used in singleplayer and for the "back to turn" feature in mu...
virtual void check_objectives() override
playturn_network_adapter network_reader_
Used by turn_data_.
virtual bool should_return_to_play_side() const override
replay_network_sender replay_sender_
Helper to send our actions to the server Used by turn_data_.
turn_info turn_data_
Helper to read and execute (in particular replay data/ user actions ) messsages from the server.
virtual void handle_generic_event(const std::string &name) override
bool end_turn_requested_
true iff the user has pressed the end turn button this turn.
static bool quit()
Shows the quit confirmation if needed.
static config get_update_shroud()
Records that the player has manually updated fog/shroud.
void sync_non_undoable()
Definition: replay.cpp:914
void add_surrender(int side_number)
Definition: replay.cpp:233
static void process_error(const std::string &msg)
Definition: replay.cpp:198
void add_countdown_update(int value, int team)
Definition: replay.cpp:239
mp_game_settings & mp_settings()
Multiplayer parameters for this game.
Definition: saved_game.hpp:60
Class for "normal" midgame saves.
Definition: savegame.hpp:254
Exception used to signal that the user has decided to abortt a game, and to load another game instead...
Definition: savegame.hpp:85
bool save_game_interactive(const std::string &message, DIALOG_TYPE dialog_type)
Save a game interactively through the savegame dialog.
Definition: savegame.cpp:377
static bool run_and_store(const std::string &commandname, const config &data, bool use_undo=true, bool show=true, synced_command::error_handler_function error_handler=default_error_function)
void set_action_bonus_count(const int count)
Definition: team.hpp:200
int action_bonus_count() const
Definition: team.hpp:199
void set_countdown_time(const int amount) const
Definition: team.hpp:197
int countdown_time() const
Definition: team.hpp:196
events::generic_event & host_transfer()
Definition: playturn.hpp:62
void send_data()
Definition: playturn.cpp:78
static PROCESS_DATA_RESULT replay_to_process_data_result(REPLAY_RETURN replayreturn)
Definition: playturn.cpp:392
PROCESS_DATA_RESULT sync_network()
Definition: playturn.cpp:59
PROCESS_DATA_RESULT
Definition: playturn.hpp:38
@ PROCESS_END_TURN
Definition: playturn.hpp:41
@ PROCESS_CANNOT_HANDLE
when we couldn't handle the given action currently.
Definition: playturn.hpp:47
@ PROCESS_RESTART_TURN
Definition: playturn.hpp:40
@ PROCESS_END_LINGER
When the host uploaded the next scenario this is returned.
Definition: playturn.hpp:43
@ PROCESS_END_LEVEL
We found a player action in the replay that caused the game to end.
Definition: playturn.hpp:49
@ PROCESS_CONTINUE
Definition: playturn.hpp:39
PROCESS_DATA_RESULT process_network_data_from_reader()
Definition: playturn.cpp:111
PROCESS_DATA_RESULT process_network_data(const config &cfg, bool chat_only=false)
Definition: playturn.cpp:126
bool receive_data(config &result)
Receives the next pending data pack from the server, if available.
void send_data(const configr_of &request)
Queues the given data to be sent to the server.
std::size_t i
Definition: function.cpp:968
Contains the exception interfaces used to signal completion of a scenario, campaign or turn.
static std::string _(const char *str)
Definition: gettext.hpp:93
An extension of playsingle_controller::hotkey_handler, which has support for MP wesnoth features like...
Standard logging facilities (interface).
int side_number
Definition: game_info.hpp:40
int add_floating_label(const floating_label &flabel)
add a label floating on the screen above everything else.
logger & info()
Definition: log.cpp:314
void turn_changed(const std::string &player_name)
compression::format save_compression_format()
Definition: game.cpp:840
bool disable_auto_moves()
Definition: general.cpp:971
replay * recorder
Definition: resources.cpp:28
std::string get_unknown_exception_type()
Utility function for finding the type of thing caught with catch(...).
Definition: general.cpp:23
std::vector< std::string > split(const config_attribute_value &val)
bool headless()
The game is running headless.
Definition: video.cpp:141
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static lg::log_domain log_engine("engine")
#define DBG_NG
#define LOG_NG
REPLAY_RETURN do_replay(bool one_move)
Definition: replay.cpp:683
void unblind()
Definition: display.hpp:1024
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
We received invalid data from wesnothd during a game This means we cannot continue with the game but ...
wesnothd_connection & connection
unsigned current_turn
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:47
Various functions that implement the undoing (and redoing) of in-game commands.
#define e