From 974269187b02059b1a3e1f17f38367e86e5bb7b9 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 13 Dec 2023 20:52:35 -0800 Subject: [PATCH] add timeout for persistent games --- src/ChatCommands.cc | 2 +- src/Lobby.cc | 37 ++++++++++++++++++++++++++- src/Lobby.hh | 9 +++++++ src/ServerState.cc | 51 +++++++++++++++++++++++--------------- src/ServerState.hh | 11 +++++++- system/config.example.json | 8 ++++++ tests/config.json | 1 + 7 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index b57c3eb6..4a3a1a6a 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -486,7 +486,7 @@ static void server_command_persist(shared_ptr c, const std::string&) { send_text_message(c, "$C6Games cannot be\npersistent if a\nquest has already\nbegun"); } else { l->toggle_flag(Lobby::Flag::PERSISTENT); - send_text_message_printf(c, "Lobby persistence\n%s", l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled"); + send_text_message_printf(l, "Lobby persistence\n%s", l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled"); } } diff --git a/src/Lobby.cc b/src/Lobby.cc index b47692e1..85254090 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -31,12 +31,21 @@ Lobby::Lobby(shared_ptr s, uint32_t id) block(0), leader_id(0), max_clients(12), - enabled_flags(0) { + enabled_flags(0), + idle_timeout_usecs(0), + idle_timeout_event( + event_new(s->base.get(), -1, EV_TIMEOUT | EV_PERSIST, &Lobby::dispatch_on_idle_timeout, this), + event_free) { + this->log.info("Created"); for (size_t x = 0; x < 12; x++) { this->next_item_id[x] = 0x00010000 + 0x00200000 * x; } } +Lobby::~Lobby() { + this->log.info("Deleted"); +} + shared_ptr Lobby::require_server_state() const { auto s = this->server_state.lock(); if (!s) { @@ -369,6 +378,12 @@ void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { send_ep3_update_game_metadata(this->shared_from_this()); } } + + // There is a player in the lobby, so it is no longer idle + if (event_pending(this->idle_timeout_event.get(), EV_TIMEOUT, nullptr)) { + event_del(this->idle_timeout_event.get()); + this->log.info("Idle timeout cancelled"); + } } void Lobby::remove_client(shared_ptr c) { @@ -409,6 +424,14 @@ void Lobby::remove_client(shared_ptr c) { send_ep3_update_game_metadata(this->shared_from_this()); } } + + // If the lobby is persistent but has an idle timeout, make it expire after + // the specified time + if ((this->count_clients() == 0) && this->check_flag(Flag::PERSISTENT) && (this->idle_timeout_usecs > 0)) { + auto tv = usecs_to_timeval(this->idle_timeout_usecs); + event_add(this->idle_timeout_event.get(), &tv); + this->log.info("Idle timeout scheduled"); + } } void Lobby::move_client_to_lobby( @@ -551,3 +574,15 @@ QuestIndex::IncludeCondition Lobby::quest_include_condition() const { return is_enabled ? QuestIndex::IncludeState::AVAILABLE : QuestIndex::IncludeState::DISABLED; }; } + +void Lobby::dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx) { + auto l = reinterpret_cast(ctx)->shared_from_this(); + if (l->count_clients() == 0) { + l->log.info("Idle timeout expired"); + auto s = l->require_server_state(); + s->remove_lobby(l); + } else { + l->log.error("Idle timeout occurred, but clients are present in lobby"); + event_del(l->idle_timeout_event.get()); + } +} diff --git a/src/Lobby.hh b/src/Lobby.hh index 6a8fae15..3aacaad8 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -130,9 +131,15 @@ struct Lobby : public std::enable_shared_from_this { // Keys in this map are client_id std::unordered_map> clients_to_add; + // This is only used when the PERSISTENT flag is set and idle_timeout_usecs + // is not zero + uint64_t idle_timeout_usecs; + std::unique_ptr idle_timeout_event; + Lobby(std::shared_ptr s, uint32_t id); Lobby(const Lobby&) = delete; Lobby(Lobby&&) = delete; + ~Lobby(); Lobby& operator=(const Lobby&) = delete; Lobby& operator=(Lobby&&) = delete; @@ -198,4 +205,6 @@ struct Lobby : public std::enable_shared_from_this { static uint8_t game_event_for_lobby_event(uint8_t lobby_event); std::unordered_map> clients_by_serial_number() const; + + static void dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx); }; diff --git a/src/ServerState.cc b/src/ServerState.cc index 08e4f38b..fa005ee0 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -18,7 +18,8 @@ using namespace std; ServerState::ServerState(shared_ptr base, const string& config_filename, bool is_replay) - : config_filename(config_filename), + : base(base), + config_filename(config_filename), is_replay(is_replay), dns_server_port(0), ip_stack_debug(false), @@ -28,6 +29,7 @@ ServerState::ServerState(shared_ptr base, const string& confi item_tracking_enabled(true), enable_drops_behavior(BehaviorSwitch::ON_BY_DEFAULT), use_server_item_tables_behavior(BehaviorSwitch::OFF_BY_DEFAULT), + persistent_game_idle_timeout_usecs(0), ep3_send_function_call_enabled(false), catch_handler_exceptions(true), ep3_infinite_meseta(false), @@ -43,6 +45,7 @@ ServerState::ServerState(shared_ptr base, const string& confi ep3_card_auction_min_size(0), ep3_card_auction_max_size(0), player_files_manager(make_shared(base)), + destroy_lobbies_event(event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this), event_free), next_lobby_id(1), pre_lobby_event(0), ep3_menu_song(-1), @@ -173,12 +176,9 @@ void ServerState::add_client_to_available_lobby(shared_ptr c) { void ServerState::remove_client_from_lobby(shared_ptr c) { auto l = c->lobby.lock(); if (l) { + uint8_t old_client_id = c->lobby_client_id; l->remove_client(c); - if (!l->check_flag(Lobby::Flag::PERSISTENT) && (l->count_clients() == 0)) { - this->remove_lobby(l->lobby_id); - } else { - send_player_leave_notification(l, c->lobby_client_id); - } + this->on_player_left_lobby(l, old_client_id); } } @@ -201,11 +201,7 @@ bool ServerState::change_client_lobby( } if (current_lobby) { - if (!current_lobby->check_flag(Lobby::Flag::PERSISTENT) && (current_lobby->count_clients() == 0)) { - this->remove_lobby(current_lobby->lobby_id); - } else { - send_player_leave_notification(current_lobby, old_lobby_client_id); - } + this->on_player_left_lobby(current_lobby, old_lobby_client_id); } if (send_join_notification) { this->send_lobby_join_notifications(new_lobby, c); @@ -256,19 +252,17 @@ shared_ptr ServerState::create_lobby() { } auto l = make_shared(this->shared_from_this(), this->next_lobby_id++); this->id_to_lobby.emplace(l->lobby_id, l); - l->log.info("Created lobby"); + l->idle_timeout_usecs = this->persistent_game_idle_timeout_usecs; return l; } -void ServerState::remove_lobby(uint32_t lobby_id) { - auto lobby_it = this->id_to_lobby.find(lobby_id); +void ServerState::remove_lobby(shared_ptr l) { + auto lobby_it = this->id_to_lobby.find(l->lobby_id); if (lobby_it == this->id_to_lobby.end()) { - throw logic_error("attempted to remove nonexistent lobby"); + throw logic_error("lobby not registered"); } - - auto l = lobby_it->second; - if (l->count_clients() != 0) { - throw logic_error("attempted to delete lobby with clients in it"); + if (lobby_it->second != l) { + throw logic_error("incorrect lobby ID in registry"); } if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { @@ -284,8 +278,20 @@ void ServerState::remove_lobby(uint32_t lobby_id) { send_ep3_disband_watcher_lobbies(l); } - l->log.info("Deleted lobby"); + this->lobbies_to_destroy.emplace(l); + auto tv = usecs_to_timeval(0); + event_add(this->destroy_lobbies_event.get(), &tv); + this->id_to_lobby.erase(lobby_it); + l->log.info("Enqueued for deletion"); +} + +void ServerState::on_player_left_lobby(shared_ptr l, uint8_t leaving_client_id) { + if (l->count_clients() > 0) { + send_player_leave_notification(l, leaving_client_id); + } else if (!l->check_flag(Lobby::Flag::PERSISTENT)) { + this->remove_lobby(l); + } } shared_ptr ServerState::find_client(const string* identifier, uint64_t serial_number, shared_ptr l) { @@ -625,6 +631,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { this->item_tracking_enabled = json.get_bool("EnableItemTracking", this->item_tracking_enabled); this->enable_drops_behavior = parse_behavior_switch("ItemDropMode", this->enable_drops_behavior); this->use_server_item_tables_behavior = parse_behavior_switch("UseServerItemTables", this->use_server_item_tables_behavior); + this->persistent_game_idle_timeout_usecs = json.get_int("PersistentGameIdleTimeout", this->persistent_game_idle_timeout_usecs); this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", this->cheat_mode_behavior); this->ep3_send_function_call_enabled = json.get_bool("EnableEpisode3SendFunctionCall", this->ep3_send_function_call_enabled); this->catch_handler_exceptions = json.get_bool("CatchHandlerExceptions", this->catch_handler_exceptions); @@ -1212,3 +1219,7 @@ shared_ptr> ServerState::information_contents_for_client(sh shared_ptr ServerState::quest_index_for_version(Version version) const { return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index; } + +void ServerState::dispatch_destroy_lobbies(evutil_socket_t, short, void* ctx) { + reinterpret_cast(ctx)->lobbies_to_destroy.clear(); +} diff --git a/src/ServerState.hh b/src/ServerState.hh index cdaea821..609c7085 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -59,6 +59,8 @@ struct ServerState : public std::enable_shared_from_this { return (b == BehaviorSwitch::OFF_BY_DEFAULT) || (b == BehaviorSwitch::ON_BY_DEFAULT); } + std::shared_ptr base; + std::string config_filename; bool is_replay; @@ -76,6 +78,7 @@ struct ServerState : public std::enable_shared_from_this { bool item_tracking_enabled; BehaviorSwitch enable_drops_behavior; BehaviorSwitch use_server_item_tables_behavior; + uint64_t persistent_game_idle_timeout_usecs; bool ep3_send_function_call_enabled; bool catch_handler_exceptions; bool ep3_infinite_meseta; @@ -173,6 +176,8 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr player_files_manager; std::unordered_map> channel_to_client; std::map> id_to_lobby; + std::unordered_set> lobbies_to_destroy; + std::shared_ptr destroy_lobbies_event; std::vector> public_lobby_search_order; std::atomic next_lobby_id; uint8_t pre_lobby_event; @@ -210,7 +215,8 @@ struct ServerState : public std::enable_shared_from_this { std::vector> all_lobbies(); std::shared_ptr create_lobby(); - void remove_lobby(uint32_t lobby_id); + void remove_lobby(std::shared_ptr l); + void on_player_left_lobby(std::shared_ptr l, uint8_t leaving_client_id); std::shared_ptr find_client( const std::string* identifier = nullptr, @@ -252,4 +258,7 @@ struct ServerState : public std::enable_shared_from_this { void load_quest_index(); void compile_functions(); void load_dol_files(); + + void enqueue_destroy_lobbies(); + static void dispatch_destroy_lobbies(evutil_socket_t, short, void* ctx); }; diff --git a/system/config.example.json b/system/config.example.json index 59efd96b..8d98719c 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -675,6 +675,14 @@ }, ], + // Persistent game timeout. This is the amount of time a game set to be + // persistent (with the $persist command) will continue to exist with no + // players in it before being deleted. The value is in microseconds; the + // default value is 30 minutes. If this is set to zero or not specified, + // persistent games never expire; such a game can then only deleted by joining + // it, running $persist again, and leaving. + "PersistentGameIdleTimeout": 1800000000, + // Cheat mode behavior. There are three values: // "Off": Cheat mode is disabled on the entire server. Cheat mode cannot be // enabled in games, and the $cheat command does nothing. This also diff --git a/tests/config.json b/tests/config.json index 66ff163a..b21bd4ec 100644 --- a/tests/config.json +++ b/tests/config.json @@ -10,6 +10,7 @@ "ServerName": "Alexandria", "CatchHandlerExceptions": false, + "PersistentGameIdleTimeout": 1800000000, "ItemDropMode": "OnByDefault", "UseServerItemTables": "OffByDefault",