The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
synced_context.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2014 - 2025
3 by David White <dave@whitevine.net>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
16#include "synced_context.hpp"
17#include "synced_commands.hpp"
18
19#include "actions/undo.hpp"
20#include "config.hpp"
21#include "game_board.hpp"
23#include "game_data.hpp"
24#include "log.hpp"
25#include "play_controller.hpp"
26#include "random.hpp"
28#include "random_synced.hpp"
29#include "replay.hpp"
30#include "resources.hpp"
31#include "seed_rng.hpp"
32#include "synced_checkup.hpp"
33#include "units/id.hpp"
34#include "utils/general.hpp"
36
37#include <cassert>
38#include <sstream>
39#include <thread>
40
41static lg::log_domain log_replay("replay");
42#define DBG_REPLAY LOG_STREAM(debug, log_replay)
43#define LOG_REPLAY LOG_STREAM(info, log_replay)
44#define WRN_REPLAY LOG_STREAM(warn, log_replay)
45#define ERR_REPLAY LOG_STREAM(err, log_replay)
46
47bool synced_context::run(const std::string& commandname, const config& data, action_spectator& spectator)
48{
49 DBG_REPLAY << "run_in_synced_context:" << commandname;
50
51 // use this after resources::recorder->add_synced_command
52 // because set_scontext_synced sets the checkup to the last added command
55
56 auto p_handler = utils::find(synced_command::registry(), commandname);
57 if(!p_handler) {
58 spectator.error("commandname [" + commandname + "] not found");
59 } else {
60 bool success = p_handler->second(data, spectator);
61 if(!success) {
62 return false;
63 }
64 }
65
67
68 sync.do_final_checkup();
69
71
72 if(undo_blocked()) {
73 // This in particular helps the networking code to make sure this command is sent.
75 }
76
77 DBG_REPLAY << "run_in_synced_context end";
78 return true;
79}
80
81bool synced_context::run_and_store(const std::string& commandname, const config& data, action_spectator& spectator)
82{
83 if(resources::controller->is_replay()) {
84 ERR_REPLAY << "ignored attempt to invoke a synced command during replay";
85 return false;
86 }
87
88 assert(resources::recorder->at_end());
90 bool success = run(commandname, data, spectator);
91 if(!success) {
93 } else {
95 }
96 return success;
97}
98
99bool synced_context::run_and_throw(const std::string& commandname, const config& data, action_spectator& spectator)
100{
101 bool success = run_and_store(commandname, data, spectator);
102 if(success) {
104 }
105
106 return success;
107}
108
110 const std::string& commandname, const config& data, action_spectator& spectator)
111{
114 return run_and_throw(commandname, data, spectator);
115 }
117 ERR_REPLAY << "trying to execute action while being in a local_choice";
118 // we reject it because such actions usually change the gamestate badly which is not intended during a
119 // local_choice. Also we cannot invoke synced commands here, because multiple clients might run local choices
120 // simultaneously so it could result in invoking different synced commands simultaneously.
121 return false;
123 synced_command::map::iterator it = synced_command::registry().find(commandname);
124 if(it == synced_command::registry().end()) {
125 spectator.error("commandname [" + commandname + "] not found");
126 return false;
127 } else {
128 return it->second(data, spectator);
129 }
130 }
131 default:
132 assert(false && "found unknown synced_context::synced_state");
133 return false;
134 }
135}
136
138{
139 static class : public action_spectator
140 {
141 public:
142 void error(const std::string& message)
143 {
144 ERR_REPLAY << "Unexpected Error during synced execution: '" << message << "'";
145 assert(!"Unexpected Error during synced execution, more info in stderr.");
146 }
147
148 } res;
149 return res;
150}
151
152namespace
153{
154class random_server_choice : public synced_context::server_choice
155{
156public:
157 /** We are in a game with no mp server and need to do this choice locally. */
158 virtual config local_choice() const override
159 {
160 return config{"new_seed", seed_rng::next_seed_str()};
161 }
162
163 /** The request which is sent to the mp server. */
164 virtual config request() const override
165 {
166 return config();
167 }
168
169 virtual const char* name() const override
170 {
171 return "random_seed";
172 }
173};
174} // namespace
175
177{
178 config retv_c = synced_context::ask_server_choice(random_server_choice());
179 config::attribute_value seed_val = retv_c["new_seed"];
180
181 return seed_val.str();
182}
183
184void synced_context::block_undo(bool do_block, bool clear_undo)
185{
186 if(!do_block) {
187 return;
188 }
189 is_undo_blocked_ = true;
190
191 if(clear_undo) {
193 }
194 // Since the action cannot be undone, send it immidiately to the other players.
196}
197
199{
200 // this method only works in a synced context.
201 assert(!is_unsynced());
202 // if we sent data of this action over the network already, undoing is blocked.
203 // if the game has ended, undoing is blocked.
204 // if the turn has ended undoing is blocked.
205
206 // Important: once this function returned true, it has to return true for the rest of the duration of the current action
207 // otherwise OOS happens, so the following code in particular relies on the inability to revoke a [end_turn]/[endlevel]
208 return is_undo_blocked_
211}
212
214{
215 // this method only works in a synced context.
216 assert(is_synced());
218}
219
224
225// TODO: this is now also used for normal actions, maybe it should be renamed.
231
232std::shared_ptr<randomness::rng> synced_context::get_rng_for_action()
233{
234 const std::string& mode = resources::classification->random_mode;
235 if(mode == "deterministic" || mode == "biased") {
236 auto get_rng = []() {
237 //rnd is nonundoable, even when the deterministic rng is used.
238 synced_context::block_undo(true, false);
240 };
241 return std::make_shared<randomness::rng_proxy>(get_rng);
242 } else {
243 return std::make_shared<randomness::synced_rng>(generate_random_seed);
244 }
245}
246
251
253{
255 "request_choice", config {
256 "request_id", request_id(),
257 name(), request(),
258 },
259 });
260}
261
263{
264 if(!is_synced()) {
265 ERR_REPLAY << "Trying to ask the server for a '" << sch.name()
266 << "' choice in a unsynced context, doing the choice locally. This can cause OOS.";
267 return sch.local_choice();
268 }
269
270 block_undo(true, false);
272 const bool is_mp_game = resources::controller->is_networked_mp();
273 bool did_require = false;
274
275 DBG_REPLAY << "ask_server for random_seed";
276
277 // There might be speak or similar commands in the replay before the user input.
278 while(true) {
280 bool is_replay_end = resources::recorder->at_end();
281
282 if(is_replay_end && !is_mp_game) {
283 // The decision is ours, and it will be inserted into the replay.
284 DBG_REPLAY << "MP synchronization: local server choice";
286 config cfg = sch.local_choice();
287 cfg["request_id"] = sch.request_id();
288 //-1 for "server" todo: change that.
290 return cfg;
291
292 } else if(is_replay_end && is_mp_game) {
293 DBG_REPLAY << "MP synchronization: remote server choice";
294
295 // Here we can get into the situation that the decision has already been made but not received yet.
297
298 // FIXME: we should call play_controller::play_silce or the application will freeze while waiting for a
299 // remote choice.
301
302 // We don't want to send multiple "require_random" to the server.
303 if(!did_require) {
304 sch.send_request();
305 did_require = true;
306 }
307
308 using namespace std::chrono_literals;
309 std::this_thread::sleep_for(10ms);
310 continue;
311
312 } else if(!is_replay_end) {
313 // The decision has already been made, and must be extracted from the replay.
314 DBG_REPLAY << "MP synchronization: replay server choice";
316
317 const config* action = resources::recorder->get_next_action();
318 if(!action) {
319 replay::process_error("[" + std::string(sch.name()) + "] expected but none found\n");
321 return sch.local_choice();
322 }
323
324 if(!action->has_child(sch.name())) {
325 replay::process_error("[" + std::string(sch.name()) + "] expected but none found, found instead:\n "
326 + action->debug() + "\n");
327
329 return sch.local_choice();
330 }
331
332 if((*action)["from_side"].str() != "server" || (*action)["side_invalid"].to_bool(false)) {
333 // we can proceed without getting OOS in this case, but allowing this would allow a "player chan choose
334 // their attack results in mp" cheat
335 replay::process_error("wrong from_side or side_invalid this could mean someone wants to cheat\n");
336 }
337
338 config res = action->mandatory_child(sch.name());
339 if(res["request_id"].to_int() != sch.request_id()) {
340 WRN_REPLAY << "Unexpected request_id: " << res["request_id"] << " expected: " << sch.request_id();
341 }
342 return res;
343 }
344 }
345}
346
348{
350 // Ai doesn't undo stuff, disabling the undo stack allows us to send moves to other clients sooner.
351 return ct.is_ai() && ct.auto_shroud_updates();
352}
353
356 , old_rng_(randomness::generator)
357{
358 LOG_REPLAY << "set_scontext_synced_base::set_scontext_synced_base";
359
360 assert(!resources::whiteboard->has_planned_unit_map());
362
365 synced_context::set_last_unit_id(resources::gameboard->unit_id_manager().get_save_id());
366
369}
370
378
381 , new_checkup_(generate_checkup("checkup"))
382 , disabler_()
383{
384 init();
385}
386
389 , new_checkup_(generate_checkup("checkup" + std::to_string(number)))
390 , disabler_()
391{
392 init();
393}
394
396{
397 if(resources::classification->oos_debug) {
398 return new mp_debug_checkup();
399 } else {
400 return new synced_checkup(resources::recorder->get_last_real_command().child_or_add(tagname));
401 }
402}
403
404/*
405 so we don't have to write the same code 3 times.
406*/
408{
409 LOG_REPLAY << "set_scontext_synced::set_scontext_synced";
410 did_final_checkup_ = false;
413}
414
416{
417 assert(!did_final_checkup_);
418 std::stringstream msg;
419 config co;
420 config cn {
421 "random_calls", new_rng_->get_random_calls(),
422 "next_unit_id", resources::gameboard->unit_id_manager().get_save_id() + 1,
423 };
424
425 if(checkup_instance->local_checkup(cn, co)) {
426 return;
427 }
428
429 if(co["random_calls"].empty()) {
430 msg << "cannot find random_calls check in replay" << std::endl;
431 } else if(co["random_calls"] != cn["random_calls"]) {
432 msg << "We called random " << new_rng_->get_random_calls() << " times, but the original game called random "
433 << co["random_calls"].to_int() << " times." << std::endl;
434 }
435
436 // Ignore empty next_unit_id to prevent false positives with older saves.
437 if(!co["next_unit_id"].empty() && co["next_unit_id"] != cn["next_unit_id"]) {
438 msg << "Our next unit id is " << cn["next_unit_id"].to_int() << " but during the original the next unit id was "
439 << co["next_unit_id"].to_int() << std::endl;
440 }
441
442 if(!msg.str().empty()) {
443 msg << co.debug() << std::endl;
444 if(dont_throw) {
445 ERR_REPLAY << msg.str();
446 } else {
447 replay::process_error(msg.str());
448 }
449 }
450
451 did_final_checkup_ = true;
452}
453
455{
456 LOG_REPLAY << "set_scontext_synced:: destructor";
457 assert(checkup_instance == &*new_checkup_);
458 if(!did_final_checkup_) {
459 // do_final_checkup(true);
460 }
462}
463
465{
466 return new_rng_->get_random_calls();
467}
468
470 : old_rng_(randomness::generator)
471{
474
475 // calling the synced rng form inside a local_choice would cause oos.
476 // TODO: should we also reset the synced checkup?
478}
479
486
488 : leaver_(synced_context::is_synced() ? new leave_synced_context() : nullptr)
489{
490}
virtual void error(const std::string &message)
Called when synced_context::run received nonsensial data based on the current gamestate.
void cleanup_action()
called after a user action, removes empty actions
Definition undo.cpp:270
void clear()
Clears the stack of undoable (and redoable) actions.
Definition undo.cpp:131
void init_action()
called before a user action, starts collecting undo steps for the new action.
Definition undo.cpp:251
void finish_action(bool can_undo)
called after a user action, pushes the collected undo steps on the undo stack.
Definition undo.cpp:258
A class to check whether the results that were calculated in the replay match the results calculated ...
virtual bool local_checkup(const config &expected_data, config &real_data)=0
Compares data to the results calculated during the original game.
Variant for storing WML attributes.
std::string str(const std::string &fallback="") const
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()
n_unit::id_manager & unit_id_manager()
bool end_turn_forced() const
const randomness::mt_rng & rng() const
Definition game_data.hpp:67
A RAII object to temporary leave the synced context like in wesnoth.synchronize_choice.
randomness::rng * old_rng_
This checkup always compares the results in from different clients in a mp game but it also causes mo...
std::size_t get_save_id() const
Used for saving id to savegame.
Definition id.cpp:42
void maybe_throw_return_to_play_side() const
int get_server_request_number() const
void check_victory()
Checks to see if a side has won.
void increase_server_request_number()
bool is_regular_game_end() const
virtual void send_to_wesnothd(const config &, const std::string &="unknown") const
virtual bool is_networked_mp() const
virtual void send_actions()
Sends replay [command]s to the server.
virtual void receive_actions()
Reads and executes replay [command]s from the server.
uint32_t get_next_random()
Get a new random number.
Definition mt_rng.cpp:62
static rng & default_instance()
Definition random.cpp:73
void add_synced_command(const std::string &name, const config &command)
Definition replay.cpp:232
void user_input(const std::string &name, const config &input, int from_side)
adds a user_input to the replay
Definition replay.cpp:242
bool at_end() const
Definition replay.cpp:631
void revert_action()
Definition replay.cpp:600
void undo()
Definition replay.cpp:556
static void process_error(const std::string &msg)
Definition replay.cpp:183
config * get_next_action()
Definition replay.cpp:607
std::shared_ptr< randomness::rng > new_rng_
A RAII object to enter the synced context, cannot be called if we are already in a synced context.
void do_final_checkup(bool dont_throw=false)
static checkup * generate_checkup(const std::string &tagname)
const std::unique_ptr< checkup > new_checkup_
This checkup compares whether the results calculated during the original game match the ones calculat...
static map & registry()
using static function variable instead of static member variable to prevent static initialization fia...
virtual const char * name() const =0
virtual config request() const =0
The request which is sent to the mp server.
virtual config local_choice() const =0
We are in a game with no mp server and need to do this choice locally.
static void set_last_unit_id(int id)
static bool undo_blocked()
static std::shared_ptr< randomness::rng > get_rng_for_action()
static config ask_server_choice(const server_choice &)
If we are in a mp game, ask the server, otherwise generate the answer ourselves.
static bool run_in_synced_context_if_not_already(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
Checks whether we are currently running in a synced context, and if not we enters it.
static bool is_undo_blocked_
As soon as get_user_choice is used with side != current_side (for example in generate_random_seed) ot...
static bool run(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
Sets the context to 'synced', initialises random context, and calls the given function.
static std::string generate_random_seed()
Generates a new seed for a synced event, by asking the 'server'.
static int get_unit_id_diff()
static synced_state get_synced_state()
static void pull_remote_user_input()
called from get_user_choice while waiting for a remove user choice.
static void reset_block_undo()
static bool is_unsynced()
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 int last_unit_id_
Used to restore the unit id manager when undoing.
static bool is_synced()
static void set_synced_state(synced_state newstate)
Should only be called form set_scontext_synced, set_scontext_local_choice.
static void send_user_choice()
called from get_user_choice to send a recently made choice to the other clients.
static bool run_and_store(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
static action_spectator & get_default_spectator()
An object to be passed to run_in_synced_context to assert false on error (the default).
static bool ignore_undo()
static bool run_and_throw(const std::string &commandname, const config &data, action_spectator &spectator=get_default_spectator())
bool is_ai() const
Definition team.hpp:292
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
Standard logging facilities (interface).
rng * generator
This generator is automatically synced during synced context.
Definition random.cpp:60
game_board * gameboard
Definition resources.cpp:20
game_data * gamedata
Definition resources.cpp:22
replay * recorder
Definition resources.cpp:28
actions::undo_list * undo_stack
Definition resources.cpp:32
game_classification * classification
Definition resources.cpp:34
play_controller * controller
Definition resources.cpp:21
std::shared_ptr< wb::manager > whiteboard
Definition resources.cpp:33
std::string next_seed_str()
Definition seed_rng.cpp:37
STL namespace.
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition general.hpp:142
std::string_view data
Definition picture.cpp:188
#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 LOG_REPLAY
Definition replay.cpp:50
#define ERR_REPLAY
Definition replay.cpp:52
Replay control code.
checkup * checkup_instance
static lg::log_domain log_replay("replay")
Various functions that implement the undoing (and redoing) of in-game commands.