The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
synced_user_choice.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2015 - 2025
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
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>
32
33static 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
39namespace
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);
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);
98 label_id_ = -1;
99 }
100 };
101}
102
103std::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*/
157config 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
235user_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 {
256 //this in particular makes sure the choices are sent immediately
258 }
259 }
260
263
264}
265
267{
268 while(!finished() && !oos_)
269 {
271 if(resources::recorder->at_end()) {
272 return;
273 }
274
275 DBG_REPLAY << "MP synchronization: extracting choice from replay with has_local_side=" << has_local_choice();
276
277 const config *action = resources::recorder->get_next_action();
278 assert(action); //action cannot be null because resources::recorder->at_end() returned false.
279 if( !action->has_child(tagname_) || !(*action)["dependent"].to_bool())
280 {
281 replay::process_error("[" + tagname_ + "] expected but none found\n. found instead:\n" + action->debug());
282 //We save this action for later
284 // execute this local choice locally
285 oos_ = true;
287 return;
288 }
289 int from_side = (*action)["from_side"].to_int(0);
290 if((*action)["side_invalid"].to_bool(false) == true)
291 {
292 //since this 'cheat' can have a quite heavy effect especially in umc content we give an oos error .
293 replay::process_error("MP synchronization: side_invalid in replay data, this could mean someone wants to cheat.\n");
294 }
295 if(required_.find(from_side) == required_.end())
296 {
297 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");
298 }
299 if(res_.find(from_side) != res_.end())
300 {
301 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");
302 }
303 res_[from_side] = action->mandatory_child(tagname_);
305 }
306}
308{
309 // there might be speak or similar commands in the replay before the user input.
315}
316
318{
319 int local_choice_prev = local_choice_;
320 //equals to any side in sides that is local, 0 if no such side exists.
321 local_choice_ = 0;
322 //if for any side from which we need an answer
323 std::vector<t_string> sides_str;
324 for(int side : required_)
325 {
326 //and we haven't already received our answer from that side
327 if(res_.find(side) == res_.end())
328 {
329 sides_str.push_back(std::to_string(side));
330 //and it is local
331 if(resources::gameboard->get_team(side).is_local() && !resources::gameboard->get_team(side).is_idle())
332 {
333 //then we have to make a local choice.
334 local_choice_ = side;
335 break;
336 }
337 }
338 }
339
340 // TRANSLATORS: In networked games, this text is shown on the map while
341 // waiting for $desc from another player.
342 // Don't end the text with a punctuation sign.
344 "waiting for $desc from side $sides",
345 "waiting for $desc from sides $sides",
346 sides_str.size(),
347 {std::pair("desc", uch_.description()), std::pair("sides", utils::format_conjunct_list("", sides_str))}
348 );
349 if(local_choice_prev != local_choice_) {
351 }
352}
353
355{
356 assert(local_choice_ != 0);
357
359 /* At least one of the decisions is ours, and it will be inserted
360 into the replay. */
361 DBG_REPLAY << "MP synchronization: local choice";
363 if(res_.find(local_choice_) != res_.end()) {
364 // It might be possible that we this choice was already made by another client while we were in uch_.query_user
365 // because our side might be reassigned while we made our choice.
366 WRN_REPLAY << "Discarding a local choice because we found it already on the replay";
367 return;
368 }
371
372 //send data to others, if needed.
374 {
376 }
378}
379
381{
382 assert(oos_);
383 ERR_REPLAY << "A sync error appeared while waiting for a synced user choice of type '" << uch_.description() << "' ([" + tagname_ + "]), doing the choice locally";
384 for(int side : required_)
385 {
386 if(res_.find(side) == res_.end())
387 {
388 ERR_REPLAY << "Doing a local choice for side " << side;
389 res_[side] = uch_.query_user(side);
390 }
391 }
392 oos_ = false;
393}
394
396{
397 user_choice_notifer_ingame notifer;
398 while(!man.finished() && man.waiting())
399 {
400 //TODO: didn't we already check for this?
401 if(!resources::gamedata->is_before_screen())
402 {
403 //during the prestart/preload event the screen is locked and we shouldn't call user_interact.
404 //because that might result in crashes if someone clicks anywhere during screenlock.
405
406 // calls man.pull via events.cpp -> pump_monitor::process
408 }
409
410 notifer.update(man.wait_message());
411 }
412}
413
415{
416 gui2::dialogs::synched_choice_wait::display(man);
417}
418
419std::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)
420{
421 const bool is_too_early = resources::gamedata->is_before_screen();
422 user_choice_manager man(name, uch, sides);
423 while(!man.finished())
424 {
425 if(man.waiting())
426 {
427 if(is_too_early) {
428 wait_prestart(man);
429 }
430 else {
431 wait_ingame(man);
432 }
433 }
434 else if(man.has_local_choice())
435 {
436 man.ask_local_choice();
437 }
438 else
439 {
440 man.fix_oos();
441 }
442 }
443 return man.res_;
444}
445
446namespace {
447 // we want to prevent calling pull() while we are already calling pull()
448 // this could for example happen if pull() receives a [side_drop] and
449 // user_choice_manager::process is called while the "player has left the game.
450 // What do you want to do?" dialog is shown.
451 static bool ucm_in_proccess = false;
452 struct ucm_process_scope {
453 ucm_process_scope() { ucm_in_proccess = true; }
454 ~ucm_process_scope() { ucm_in_proccess = false; }
455 };
456}
458{
459 if(!oos_ && !finished() && !ucm_in_proccess)
460 {
461 ucm_process_scope scope1;
462 pull();
463 }
464}
int current_side_
Definition move.cpp:406
double t
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
std::string debug() const
Definition config.cpp:1214
bool has_child(std::string_view key) const
Determine whether a config has a child or not.
Definition config.cpp:312
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition config.cpp:362
virtual void play_slice()
rect map_outside_area() const
Returns the available area for a map, this may differ from the above.
Definition display.cpp:516
static display * get_singleton()
Returns the display object if a display object exists.
Definition display.hpp:102
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 rect &r)
void set_font_size(int font_size)
virtual const std::vector< team > & teams() const override
team & get_team(int i)
bool is_before_screen() const
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:242
void revert_action()
Definition replay.cpp:600
static void process_error(const std::string &msg)
Definition replay.cpp:183
config * get_next_action()
Definition replay.cpp:607
static bool undo_blocked()
static void pull_remote_user_input()
called from get_user_choice while waiting for a remove user choice.
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 bool is_synced()
static void send_user_choice()
called from get_user_choice to send a recently made choice to the other clients.
This class stores all the data for a single 'side' (in game nomenclature).
Definition team.hpp:74
bool is_idle() const
Definition team.hpp:311
bool is_empty() const
Definition team.hpp:286
events::generic_event changed_event_
bool waiting() const
Note: currently finished() does not imply !waiting() so you may need to check both.
const std::string & wait_message() const
user_choice_manager(const std::string &name, const mp_sync::user_choice &uch, const std::set< int > &sides)
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 & tagname_
map_display and display: classes which take care of displaying the map and game-data on the screen.
const config * cfg
#define VNGETTEXT(msgid, msgid_plural, count,...)
Standard logging facilities (interface).
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
#define WRN_REPLAY
Definition replay.cpp:51
REPLAY_RETURN do_replay_handle(bool one_move)
Definition replay.cpp:709
#define DBG_REPLAY
Definition replay.cpp:49
#define ERR_REPLAY
Definition replay.cpp:52
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
An abstract description of a rectangle with integer coordinates.
Definition rect.hpp:49
static void wait_ingame(user_choice_manager &man)
static void wait_prestart(user_choice_manager &man)
#define DBG_REPLAY
static lg::log_domain log_replay("replay")
#define LOG_REPLAY
#define ERR_REPLAY