wesnothd: update current turn in init_side instead of end_turn

This matches the behavior on the client where the events that
happen after end_turn (the "turn end" wml events) happen during
the last sides turn not the next sides turn. In particular this
fixes #2563. It also fixes #3899 because the new logic
automaticially skips empty sides already in init_side and not on
end_turn.

It also changes how description is updated, since carrying that
return value arround specifying whether it was updated was becoming
annoying.

With a few code improvements from soliton.
This commit is contained in:
gfgtdf 2024-01-14 03:30:33 +01:00
parent f2bcc997b8
commit e64283534f
4 changed files with 153 additions and 80 deletions

View file

@ -21,6 +21,7 @@
#include "serialization/chrono.hpp"
#include "server/wesnothd/player_network.hpp"
#include "server/wesnothd/server.hpp"
#include "utils/math.hpp"
#include <iomanip>
#include <sstream>
@ -97,8 +98,10 @@ game::game(wesnothd::server& server, player_connections& player_connections,
, history_()
, chat_history_()
, description_(nullptr)
, description_updated_(false)
, current_turn_(0)
, current_side_index_(0)
, next_side_index_(0)
, num_turns_(0)
, all_observers_muted_(false)
, bans_()
@ -306,15 +309,24 @@ void game::start_game(player_iterator starter)
DBG_GAME << "Number of sides: " << nsides_;
int turn = 1;
int side = 0;
int next_side = 0;
// Savegames have a snapshot that tells us which side starts.
if(const simple_wml::node* snapshot = level_.root().child("snapshot")) {
turn = lexical_cast_default<int>((*snapshot)["turn_at"], 1);
side = lexical_cast_default<int>((*snapshot)["playing_team"], 0);
LOG_GAME << "Reload from turn: " << turn << ". Current side is: " << side + 1 << ".";
if((*snapshot)["init_side_done"].to_bool(false)) {
next_side = -1;
} else {
next_side = side;
}
LOG_GAME << "Reload from turn: " << turn << ". Current side is: " << side + 1 << ". Next side is: " << next_side + 1;
}
current_turn_ = turn;
current_side_index_ = side;
next_side_index_ = next_side;
num_turns_ = lexical_cast_default<int>((*starting_pos(level_.root()))["turns"], -1);
update_turn_data();
@ -632,10 +644,10 @@ void game::notify_new_host()
send_and_record_server_message(message);
}
bool game::describe_slots()
void game::describe_slots()
{
if(started_ || description_ == nullptr) {
return false;
return;
}
int available_slots = 0;
@ -652,15 +664,10 @@ bool game::describe_slots()
++i;
}
simple_wml::node* slots_cfg = description_->child("slot_data");
if(!slots_cfg) {
slots_cfg = &description_->add_child("slot_data");
}
simple_wml::node& slots_cfg = description_for_writing()->child_or_add("slot_data");
slots_cfg->set_attr_int("vacant", available_slots);
slots_cfg->set_attr_int("max", num_sides);
return true;
slots_cfg.set_attr_int("vacant", available_slots);
slots_cfg.set_attr_int("max", num_sides);
}
bool game::player_is_banned(player_iterator player, const std::string& name) const
@ -902,11 +909,16 @@ bool game::is_legal_command(const simple_wml::node& command, player_iterator use
return false;
}
if(from_side_index >= sides_.size() || sides_[from_side_index] != user) {
if(get_side_player(from_side_index) != user) {
return false;
}
}
if(command.child("init_side")) {
// If side init was already done, the rhs returns nullopt so this also return fale in this case.
return get_side_player(get_next_side_index()) == user;
}
if(is_current) {
return true;
}
@ -929,7 +941,7 @@ bool game::is_legal_command(const simple_wml::node& command, player_iterator use
}
std::size_t side_number = sn.to_int();
if(side_number >= sides_.size() || sides_[side_number] != user) {
if(get_side_player(side_number) != user) {
return false;
} else {
return true;
@ -1052,7 +1064,10 @@ bool game::process_turn(simple_wml::document& data, player_iterator user)
send_and_record_server_message(username(user) + " has surrendered.");
} else if(is_current_player(user) && (*command).child("end_turn")) {
simple_wml::node& endturn = *(*command).child("end_turn");
turn_ended = end_turn(endturn["next_player_number"].to_int());
end_turn(endturn["next_player_number"].to_int());
} else if(command->child("init_side")) {
//["side_number"]
init_turn();
}
++index;
@ -1295,46 +1310,34 @@ void game::process_change_turns_wml(simple_wml::document& data, player_iterator
assert(static_cast<int>(this->current_turn()) == current_turn);
simple_wml::node* turns_cfg = description_->child("turn_data");
if(!turns_cfg) {
turns_cfg = &description_->add_child("turn_data");
}
simple_wml::node& turns_cfg = description_for_writing()->child_or_add("turn_data");
ctw_node.copy_into(*turns_cfg);
ctw_node.copy_into(turns_cfg);
// Don't send or store this change, all players should have gotten it by wml.
}
bool game::end_turn(int new_side)
void game::end_turn(int new_side)
{
if(new_side > 0) {
current_side_index_ = new_side - 1;
next_side_index_ = new_side - 1;
} else {
next_side_index_ = current_side_index_ + 1;
}
else {
++current_side_index_;
}
void game::init_turn()
{
int new_side = get_next_side_index();
if(new_side > current_side_index_) {
++current_turn_;
}
// Skip over empty sides.
for(int i = 0; i < nsides_ && side_controllers_[current_side()] == side_controller::type::none; ++i) {
++current_side_index_;
}
auto res = std::div(current_side_index_, nsides_ > 0 ? nsides_ : 1);
if(res.quot == 0) {
return false;
}
current_side_index_ = res.rem;
current_turn_ += res.quot;
if(description_ == nullptr) {
// TODO: why do we need this?
return false;
}
current_side_index_ = new_side;
next_side_index_ = -1;
update_turn_data();
return true;
}
void game::update_turn_data()
@ -1343,13 +1346,10 @@ void game::update_turn_data()
return;
}
simple_wml::node* turns_cfg = description_->child("turn_data");
if(!turns_cfg) {
turns_cfg = &description_->add_child("turn_data");
}
simple_wml::node& turns_cfg = description_for_writing()->child_or_add("turn_data");
turns_cfg->set_attr_int("current", current_turn());
turns_cfg->set_attr_int("max", num_turns_);
turns_cfg.set_attr_int("current", current_turn());
turns_cfg.set_attr_int("max", num_turns_);
}
bool game::add_player(player_iterator player, bool observer)
@ -1998,4 +1998,26 @@ bool game::is_reload() const
return multiplayer.has_attr("savegame") && multiplayer["savegame"].to_bool();
}
int game::get_next_side_index() const
{
return get_next_nonempty(next_side_index_);
}
int game::get_next_nonempty(int side_index) const
{
if(side_index == -1) {
return -1;
}
if(nsides_ == 0) {
return 0;
}
for(int i = 0; i < nsides_; ++i) {
int res = modulo(side_index + i, nsides_, 0);
if(side_controllers_[res] != side_controller::type::none) {
return res;
}
}
return -1;
}
} // namespace wesnothd

View file

@ -363,10 +363,8 @@ public:
/**
* Set the description to the number of available slots.
*
* @returns True if the number of slots has changed.
*/
bool describe_slots();
void describe_slots();
/**
* Sends a message to all players in this game that aren't excluded.
@ -482,6 +480,27 @@ public:
return description_;
}
/**
* @return The node containing the game's current description. and remembers that it was changed.
*/
simple_wml::node* description_for_writing()
{
description_updated_ = true;
return description_;
}
/**
* @return The node containing the game's current description if it was changed.
*/
simple_wml::node* changed_description()
{
if(description_updated_) {
description_updated_ = false;
return description_;
}
return nullptr;
}
/**
* Sets the password required to access the game.
*
@ -602,7 +621,19 @@ private:
*/
std::size_t current_side() const
{
return nsides_ != 0 ? (current_side_index_ % nsides_) : 0;
// At the start of the game it can happen that current_side_index_ is 0,
// but the first side is empty. It's better to do this than to skip empty
// sides in start_game() in case the controller changes during start events.
return get_next_nonempty(current_side_index_);
}
/**
* @return The player who owns the side at index @a index.
* nullopt if wither index is invalid or the side is not owned.
*/
utils::optional<player_iterator> get_side_player(size_t index) const
{
return index >= sides_.size() ? utils::optional<player_iterator>() : sides_[index];
}
/**
@ -610,7 +641,8 @@ private:
*/
utils::optional<player_iterator> current_player() const
{
return sides_[current_side()];
// sides_ should never be empty but just to be sure.
return get_side_player(current_side());
}
/**
@ -742,13 +774,17 @@ private:
/**
* Function which should be called every time a player ends their turn
* (i.e. [end_turn] received). This will update the 'turn' attribute for
* the game's description when appropriate.
* (i.e. [end_turn] received).
*
* @param new_side The side number whose turn to move it has become.
* @return True if the current side and-or current turn values have been updated, false otherwise.
*/
bool end_turn(int new_side);
void end_turn(int new_side);
/**
* Function which should be called every time a player starts their turn
* (i.e. [init_side] received). This will update the 'turn' attribute for
* the game's description when appropriate.
*/
void init_turn();
/**
* Set or update the current and max turn values in the game's description.
@ -792,6 +828,13 @@ private:
*/
std::string debug_sides_info() const;
/// @return the side index for which we accept [init_side]
int get_next_side_index() const;
/**
* finds the first side starting at @a side_index that is non empty.
*/
int get_next_nonempty(int side_index) const;
/** The wesnothd server instance this game exists on. */
wesnothd::server& server;
player_connections& player_connections_;
@ -869,10 +912,18 @@ private:
/** Pointer to the game's description in the games_and_users_list_. */
simple_wml::node* description_;
/** Set to true whenever description_ was changed that an update needs to be sent to clients. */
bool description_updated_;
/** The game's current turn. */
int current_turn_;
/** The index of the current side. The side number is current_side_index_+1. */
int current_side_index_;
/**
* after [end_turn] was received, this contains the side for who we accept [init_side].
* -1 if we currently don't accept [init_side] because the current player didn't end his turn yet.
**/
int next_side_index_;
/** The maximum number of turns before the game ends. */
int num_turns_;
/** Whether all observers should be treated as muted. */

View file

@ -1506,7 +1506,7 @@ void server::handle_join_game(player_iterator player, simple_wml::node& join)
// send notification of changes to the game and user
simple_wml::document diff;
bool diff1 = make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", g->description(), diff);
bool diff1 = make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", g->changed_description(), diff);
bool diff2 = make_change_diff(games_and_users_list_.root(), nullptr, "user",
player->info().config_address(), diff);
@ -1572,7 +1572,7 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
assert(games_and_users_list_.child("gamelist")->children("game").empty() == false);
simple_wml::node& desc = *g.description();
simple_wml::node& desc = *g.description_for_writing();
// Update the game's description.
// If there is no shroud, then tell players in the lobby
@ -1659,7 +1659,7 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
return;
}
simple_wml::node& desc = *g.description();
simple_wml::node& desc = *g.description_for_writing();
// Update the game's description.
if(const simple_wml::node* m = scenario->child("multiplayer")) {
@ -1790,7 +1790,8 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
if(g.remove_player(p)) {
delete_game(g.id());
} else {
auto description = g.description();
bool has_diff = false;
simple_wml::document diff;
// After this line, the game object may be destroyed. Don't use `g`!
player_connections_.modify(p, std::bind(&player_record::enter_lobby, std::placeholders::_1));
@ -1798,14 +1799,14 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
// Only run this if the game object is still valid
if(auto gStrong = g_ptr.lock()) {
gStrong->describe_slots();
//Don't update the game if it no longer exists.
has_diff |= make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", gStrong->description(), diff);
}
// Send all other players in the lobby the update to the gamelist.
simple_wml::document diff;
bool diff1 = make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", description, diff);
bool diff2 = make_change_diff(games_and_users_list_.root(), nullptr, "user", player.config_address(), diff);
has_diff |= make_change_diff(games_and_users_list_.root(), nullptr, "user", player.config_address(), diff);
if(diff1 || diff2) {
if(has_diff) {
send_to_lobby(diff, p);
}
@ -1827,9 +1828,8 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
g.update_side_data();
}
if(g.describe_slots()) {
update_game_in_lobby(g);
}
g.describe_slots();
update_game_in_lobby(g);
g.send_data(data, p);
return;
@ -1840,9 +1840,8 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
// If the owner of a side is changing the controller.
} else if(const simple_wml::node* change = data.child("change_controller")) {
g.transfer_side_control(p, *change);
if(g.describe_slots()) {
update_game_in_lobby(g);
}
g.describe_slots();
update_game_in_lobby(g);
return;
// If all observers should be muted. (toggles)
@ -1871,9 +1870,9 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
if(user) {
player_connections_.modify(*user, std::bind(&player_record::enter_lobby, std::placeholders::_1));
if(g.describe_slots()) {
update_game_in_lobby(g, user);
}
g.describe_slots();
update_game_in_lobby(g, user);
// Send all other players in the lobby the update to the gamelist.
simple_wml::document gamelist_diff;
@ -1911,9 +1910,8 @@ void server::handle_player_in_game(player_iterator p, simple_wml::document& data
// Notify the game of the commands, and if it changes
// the description, then sync the new description
// to players in the lobby.
if(g.process_turn(data, p)) {
update_game_in_lobby(g);
}
g.process_turn(data, p);
update_game_in_lobby(g);
return;
} else if(data.child("whiteboard")) {
@ -2996,11 +2994,13 @@ void server::delete_game(int gameid, const std::string& reason)
}
}
void server::update_game_in_lobby(const wesnothd::game& g, utils::optional<player_iterator> exclude)
void server::update_game_in_lobby(wesnothd::game& g, utils::optional<player_iterator> exclude)
{
simple_wml::document diff;
if(make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", g.description(), diff)) {
send_to_lobby(diff, exclude);
if(auto p_desc = g.changed_description()) {
if(make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", p_desc, diff)) {
send_to_lobby(diff, exclude);
}
}
}

View file

@ -222,7 +222,7 @@ private:
void delete_game(int, const std::string& reason="");
void update_game_in_lobby(const game& g, utils::optional<player_iterator> exclude = {});
void update_game_in_lobby(game& g, utils::optional<player_iterator> exclude = {});
void start_new_server();