/* Copyright (C) 2019 Mr Goldberg This file is part of the Goldberg Emulator The Goldberg Emulator is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. The Goldberg Emulator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the Goldberg Emulator; if not, see . */ #include "steam_matchmaking_servers.h" static void network_callback(void *object, Common_Message *msg) { PRINT_DEBUG("steam_matchmaking_servers_callback\n"); Steam_Matchmaking_Servers *obj = (Steam_Matchmaking_Servers *)object; obj->Callback(msg); } Steam_Matchmaking_Servers::Steam_Matchmaking_Servers(class Settings *settings, class Networking *network) { this->settings = settings; this->network = network; this->network->setCallback(CALLBACK_ID_GAMESERVER, (uint64) 0, &network_callback, this); } static int server_list_request; // Request a new list of servers of a particular type. These calls each correspond to one of the EMatchMakingType values. // Each call allocates a new asynchronous request object. // Request object must be released by calling ReleaseRequest( hServerListRequest ) HServerListRequest Steam_Matchmaking_Servers::RequestInternetServerList( AppId_t iApp, STEAM_ARRAY_COUNT(nFilters) MatchMakingKeyValuePair_t **ppchFilters, uint32 nFilters, ISteamMatchmakingServerListResponse *pRequestServersResponse ) { PRINT_DEBUG("RequestInternetServerList\n"); //TODO return RequestLANServerList(iApp, pRequestServersResponse); } HServerListRequest Steam_Matchmaking_Servers::RequestLANServerList( AppId_t iApp, ISteamMatchmakingServerListResponse *pRequestServersResponse ) { PRINT_DEBUG("RequestLANServerList %u\n", iApp); std::lock_guard lock(global_mutex); struct Steam_Matchmaking_Request request; request.appid = iApp; request.callbacks = pRequestServersResponse; request.cancelled = false; request.completed = false; requests.push_back(request); ++server_list_request; requests[requests.size() - 1].id = (void *)server_list_request; HServerListRequest id = requests[requests.size() - 1].id; PRINT_DEBUG("request id: %p\n", id); return id; } HServerListRequest Steam_Matchmaking_Servers::RequestFriendsServerList( AppId_t iApp, STEAM_ARRAY_COUNT(nFilters) MatchMakingKeyValuePair_t **ppchFilters, uint32 nFilters, ISteamMatchmakingServerListResponse *pRequestServersResponse ) { PRINT_DEBUG("RequestFriendsServerList\n"); //TODO return RequestLANServerList(iApp, pRequestServersResponse); } HServerListRequest Steam_Matchmaking_Servers::RequestFavoritesServerList( AppId_t iApp, STEAM_ARRAY_COUNT(nFilters) MatchMakingKeyValuePair_t **ppchFilters, uint32 nFilters, ISteamMatchmakingServerListResponse *pRequestServersResponse ) { PRINT_DEBUG("RequestFavoritesServerList\n"); //TODO return RequestLANServerList(iApp, pRequestServersResponse); } HServerListRequest Steam_Matchmaking_Servers::RequestHistoryServerList( AppId_t iApp, STEAM_ARRAY_COUNT(nFilters) MatchMakingKeyValuePair_t **ppchFilters, uint32 nFilters, ISteamMatchmakingServerListResponse *pRequestServersResponse ) { PRINT_DEBUG("RequestHistoryServerList\n"); //TODO return RequestLANServerList(iApp, pRequestServersResponse); } HServerListRequest Steam_Matchmaking_Servers::RequestSpectatorServerList( AppId_t iApp, STEAM_ARRAY_COUNT(nFilters) MatchMakingKeyValuePair_t **ppchFilters, uint32 nFilters, ISteamMatchmakingServerListResponse *pRequestServersResponse ) { PRINT_DEBUG("RequestSpectatorServerList\n"); //TODO return RequestLANServerList(iApp, pRequestServersResponse); } // Releases the asynchronous request object and cancels any pending query on it if there's a pending query in progress. // RefreshComplete callback is not posted when request is released. void Steam_Matchmaking_Servers::ReleaseRequest( HServerListRequest hServerListRequest ) { PRINT_DEBUG("ReleaseRequest %p\n", hServerListRequest); auto g = std::begin(requests); while (g != std::end(requests)) { if (g->id == hServerListRequest) { //NOTE: some garbage games release the request before getting server details from it. // g = requests.erase(g); // } else { //TODO: eventually delete the released request. g->cancelled = true; g->released = true; } ++g; } } /* the filter operation codes that go in the key part of MatchMakingKeyValuePair_t should be one of these: "map" - Server passes the filter if the server is playing the specified map. "gamedataand" - Server passes the filter if the server's game data (ISteamGameServer::SetGameData) contains all of the specified strings. The value field is a comma-delimited list of strings to match. "gamedataor" - Server passes the filter if the server's game data (ISteamGameServer::SetGameData) contains at least one of the specified strings. The value field is a comma-delimited list of strings to match. "gamedatanor" - Server passes the filter if the server's game data (ISteamGameServer::SetGameData) does not contain any of the specified strings. The value field is a comma-delimited list of strings to check. "gametagsand" - Server passes the filter if the server's game tags (ISteamGameServer::SetGameTags) contains all of the specified strings. The value field is a comma-delimited list of strings to check. "gametagsnor" - Server passes the filter if the server's game tags (ISteamGameServer::SetGameTags) does not contain any of the specified strings. The value field is a comma-delimited list of strings to check. "and" (x1 && x2 && ... && xn) "or" (x1 || x2 || ... || xn) "nand" !(x1 && x2 && ... && xn) "nor" !(x1 || x2 || ... || xn) - Performs Boolean operation on the following filters. The operand to this filter specifies the "size" of the Boolean inputs to the operation, in Key/value pairs. (The keyvalue pairs must immediately follow, i.e. this is a prefix logical operator notation.) In the simplest case where Boolean expressions are not nested, this is simply the number of operands. For example, to match servers on a particular map or with a particular tag, would would use these filters. ( server.map == "cp_dustbowl" || server.gametags.contains("payload") ) "or", "2" "map", "cp_dustbowl" "gametagsand", "payload" If logical inputs are nested, then the operand specifies the size of the entire "length" of its operands, not the number of immediate children. ( server.map == "cp_dustbowl" || ( server.gametags.contains("payload") && !server.gametags.contains("payloadrace") ) ) "or", "4" "map", "cp_dustbowl" "and", "2" "gametagsand", "payload" "gametagsnor", "payloadrace" Unary NOT can be achieved using either "nand" or "nor" with a single operand. "addr" - Server passes the filter if the server's query address matches the specified IP or IP:port. "gameaddr" - Server passes the filter if the server's game address matches the specified IP or IP:port. The following filter operations ignore the "value" part of MatchMakingKeyValuePair_t "dedicated" - Server passes the filter if it passed true to SetDedicatedServer. "secure" - Server passes the filter if the server is VAC-enabled. "notfull" - Server passes the filter if the player count is less than the reported max player count. "hasplayers" - Server passes the filter if the player count is greater than zero. "noplayers" - Server passes the filter if it doesn't have any players. "linux" - Server passes the filter if it's a linux server */ void Steam_Matchmaking_Servers::server_details(Gameserver *g, gameserveritem_t *server) { uint16 query_port = g->query_port(); if (g->query_port() == 0xFFFF) { query_port = g->port(); } server->m_NetAdr.Init(g->ip(), query_port, g->port()); server->m_nPing = 10; //TODO server->m_bHadSuccessfulResponse = true; server->m_bDoNotRefresh = false; strncpy(server->m_szGameDir, g->mod_dir().c_str(), k_cbMaxGameServerGameDir - 1); strncpy(server->m_szMap, g->map_name().c_str(), k_cbMaxGameServerMapName - 1); strncpy(server->m_szGameDescription, g->game_description().c_str(), k_cbMaxGameServerGameDescription - 1); server->m_szGameDir[k_cbMaxGameServerGameDir - 1] = 0; server->m_szMap[k_cbMaxGameServerMapName - 1] = 0; server->m_szGameDescription[k_cbMaxGameServerGameDescription - 1] = 0; server->m_nAppID = g->appid(); server->m_nPlayers = g->num_players(); server->m_nMaxPlayers = g->max_player_count(); server->m_nBotPlayers = g->bot_player_count(); server->m_bPassword = g->password_protected(); server->m_bSecure = g->secure(); server->m_ulTimeLastPlayed = 0; server->m_nServerVersion = g->version(); server->SetName(g->server_name().c_str()); server->m_steamID = CSteamID((uint64)g->id()); PRINT_DEBUG("server_details %llu\n", g->id()); strncpy(server->m_szGameTags, g->tags().c_str(), k_cbMaxGameServerTags - 1); server->m_szGameTags[k_cbMaxGameServerTags - 1] = 0; } // Get details on a given server in the list, you can get the valid range of index // values by calling GetServerCount(). You will also receive index values in // ISteamMatchmakingServerListResponse::ServerResponded() callbacks gameserveritem_t *Steam_Matchmaking_Servers::GetServerDetails( HServerListRequest hRequest, int iServer ) { PRINT_DEBUG("GetServerDetails %p %i\n", hRequest, iServer); std::lock_guard lock(global_mutex); std::vector gameservers_filtered; auto g = std::begin(requests); while (g != std::end(requests)) { PRINT_DEBUG("equal? %p %p\n", hRequest, g->id); if (g->id == hRequest) { gameservers_filtered = g->gameservers_filtered; PRINT_DEBUG("found %u\n", gameservers_filtered.size()); break; } ++g; } if (iServer >= gameservers_filtered.size() || iServer < 0) { return NULL; } Gameserver *gs = &gameservers_filtered[iServer].server; gameserveritem_t *server = new gameserveritem_t(); //TODO: is the new here ok? server_details(gs, server); PRINT_DEBUG("Returned server details\n"); return server; } // Cancel an request which is operation on the given list type. You should call this to cancel // any in-progress requests before destructing a callback object that may have been passed // to one of the above list request calls. Not doing so may result in a crash when a callback // occurs on the destructed object. // Canceling a query does not release the allocated request handle. // The request handle must be released using ReleaseRequest( hRequest ) void Steam_Matchmaking_Servers::CancelQuery( HServerListRequest hRequest ) { PRINT_DEBUG("CancelQuery %p\n", hRequest); auto g = std::begin(requests); while (g != std::end(requests)) { if (g->id == hRequest) { g->cancelled = true; } ++g; } } // Ping every server in your list again but don't update the list of servers // Query callback installed when the server list was requested will be used // again to post notifications and RefreshComplete, so the callback must remain // valid until another RefreshComplete is called on it or the request // is released with ReleaseRequest( hRequest ) void Steam_Matchmaking_Servers::RefreshQuery( HServerListRequest hRequest ) { PRINT_DEBUG("RefreshQuery %p\n", hRequest); } // Returns true if the list is currently refreshing its server list bool Steam_Matchmaking_Servers::IsRefreshing( HServerListRequest hRequest ) { PRINT_DEBUG("IsRefreshing %p\n", hRequest); return false; } // How many servers in the given list, GetServerDetails above takes 0... GetServerCount() - 1 int Steam_Matchmaking_Servers::GetServerCount( HServerListRequest hRequest ) { PRINT_DEBUG("GetServerCount %p\n", hRequest); std::lock_guard lock(global_mutex); int size = 0; auto g = std::begin(requests); while (g != std::end(requests)) { if (g->id == hRequest) { size = g->gameservers_filtered.size(); break; } ++g; } return size; } // Refresh a single server inside of a query (rather than all the servers ) void Steam_Matchmaking_Servers::RefreshServer( HServerListRequest hRequest, int iServer ) { PRINT_DEBUG("RefreshServer %p\n", hRequest); //TODO } static HServerQuery new_server_query() { static int a; ++a; if (!a) ++a; return a; } //----------------------------------------------------------------------------- // Queries to individual servers directly via IP/Port //----------------------------------------------------------------------------- // Request updated ping time and other details from a single server HServerQuery Steam_Matchmaking_Servers::PingServer( uint32 unIP, uint16 usPort, ISteamMatchmakingPingResponse *pRequestServersResponse ) { PRINT_DEBUG("PingServer %hhu.%hhu.%hhu.%hhu:%hu\n", ((unsigned char *)&unIP)[3], ((unsigned char *)&unIP)[2], ((unsigned char *)&unIP)[1], ((unsigned char *)&unIP)[0], usPort); std::lock_guard lock(global_mutex); Steam_Matchmaking_Servers_Direct_IP_Request r; r.id = new_server_query(); r.ip = unIP; r.port = usPort; r.ping_response = pRequestServersResponse; direct_ip_requests.push_back(r); return r.id; } // Request the list of players currently playing on a server HServerQuery Steam_Matchmaking_Servers::PlayerDetails( uint32 unIP, uint16 usPort, ISteamMatchmakingPlayersResponse *pRequestServersResponse ) { PRINT_DEBUG("PlayerDetails %hhu.%hhu.%hhu.%hhu:%hu\n", ((unsigned char *)&unIP)[3], ((unsigned char *)&unIP)[2], ((unsigned char *)&unIP)[1], ((unsigned char *)&unIP)[0], usPort); std::lock_guard lock(global_mutex); Steam_Matchmaking_Servers_Direct_IP_Request r; r.id = new_server_query(); r.ip = unIP; r.port = usPort; r.players_response = pRequestServersResponse; direct_ip_requests.push_back(r); return r.id; } // Request the list of rules that the server is running (See ISteamGameServer::SetKeyValue() to set the rules server side) HServerQuery Steam_Matchmaking_Servers::ServerRules( uint32 unIP, uint16 usPort, ISteamMatchmakingRulesResponse *pRequestServersResponse ) { PRINT_DEBUG("ServerRules %hhu.%hhu.%hhu.%hhu:%hu\n", ((unsigned char *)&unIP)[3], ((unsigned char *)&unIP)[2], ((unsigned char *)&unIP)[1], ((unsigned char *)&unIP)[0], usPort); std::lock_guard lock(global_mutex); Steam_Matchmaking_Servers_Direct_IP_Request r; r.id = new_server_query(); r.ip = unIP; r.port = usPort; r.rules_response = pRequestServersResponse; direct_ip_requests.push_back(r); return r.id; } // Cancel an outstanding Ping/Players/Rules query from above. You should call this to cancel // any in-progress requests before destructing a callback object that may have been passed // to one of the above calls to avoid crashing when callbacks occur. void Steam_Matchmaking_Servers::CancelServerQuery( HServerQuery hServerQuery ) { PRINT_DEBUG("CancelServerQuery\n"); std::lock_guard lock(global_mutex); auto r = std::find_if(direct_ip_requests.begin(), direct_ip_requests.end(), [&hServerQuery](Steam_Matchmaking_Servers_Direct_IP_Request const& item) { return item.id == hServerQuery; }); if (direct_ip_requests.end() == r) return; direct_ip_requests.erase(r); } void Steam_Matchmaking_Servers::RunCallbacks() { PRINT_DEBUG("Steam_Matchmaking_Servers::RunCallbacks\n"); { auto g = std::begin(gameservers); while (g != std::end(gameservers)) { if (check_timedout(g->last_recv, SERVER_TIMEOUT)) { g = gameservers.erase(g); PRINT_DEBUG("SERVER TIMEOUT\n"); } else { ++g; } } } PRINT_DEBUG("REQUESTS %zu gs: %zu\n", requests.size(), gameservers.size()); for (auto &r : requests) { if (r.cancelled || r.completed) continue; r.gameservers_filtered.clear(); for (auto &g : gameservers) { if (g.server.appid() == r.appid) { PRINT_DEBUG("REQUESTS server found\n"); r.gameservers_filtered.push_back(g); } } } std::vector requests_temp(requests); PRINT_DEBUG("REQUESTS_TEMP %zu\n", requests_temp.size()); for (auto &r : requests) { r.completed = true; } for (auto &r : requests_temp) { if (r.cancelled || r.completed) continue; int i = 0; for (auto &g : r.gameservers_filtered) { PRINT_DEBUG("REQUESTS server responded cb %p\n", r.id); r.callbacks->ServerResponded(r.id, i); ++i; } if (i) { r.callbacks->RefreshComplete(r.id, eServerResponded); } else { r.callbacks->RefreshComplete(r.id, eNoServersListedOnMasterServer); } } std::vector direct_ip_requests_temp = direct_ip_requests; direct_ip_requests.clear(); for (auto &r : direct_ip_requests_temp) { PRINT_DEBUG("dip request: %lu:%hu\n", r.ip, r.port); for (auto &g : gameservers) { PRINT_DEBUG("server: %lu:%hu\n", g.server.ip(), g.server.query_port()); uint16 query_port = g.server.query_port(); if (query_port == 0xFFFF) { query_port = g.server.port(); } if (query_port == r.port && g.server.ip() == r.ip) { if (r.rules_response) { int number_rules = g.server.values().size(); PRINT_DEBUG("rules: %lu\n", number_rules); auto rule = g.server.values().begin(); for (int i = 0; i < number_rules; ++i) { PRINT_DEBUG("RULE %s %s\n", rule->first.c_str(), rule->second.c_str()); r.rules_response->RulesResponded(rule->first.c_str(), rule->second.c_str()); ++rule; } r.rules_response->RulesRefreshComplete(); r.rules_response = NULL; } if (r.ping_response) { gameserveritem_t server; server_details(&(g.server), &server); r.ping_response->ServerResponded(server); r.ping_response = NULL; } //TODO: players response } } if (r.rules_response) r.rules_response->RulesRefreshComplete(); //TODO: player response if (r.players_response) r.players_response->PlayersRefreshComplete(); if (r.ping_response) r.ping_response->ServerFailedToRespond(); } } void Steam_Matchmaking_Servers::Callback(Common_Message *msg) { if (msg->has_gameserver()) { if (msg->gameserver().offline()) { for (auto &g : gameservers) { if (g.server.id() == msg->gameserver().id()) { g.last_recv = std::chrono::high_resolution_clock::time_point(); } } } else { bool already = false; for (auto &g : gameservers) { if (g.server.id() == msg->gameserver().id()) { g.last_recv = std::chrono::high_resolution_clock::now(); g.server = msg->gameserver(); g.server.set_ip(msg->source_ip()); already = true; } } if (!already) { struct Steam_Matchmaking_Servers_Gameserver g; g.last_recv = std::chrono::high_resolution_clock::now(); g.server = msg->gameserver(); g.server.set_ip(msg->source_ip()); gameservers.push_back(g); PRINT_DEBUG("SERVER ADDED\n"); } } } }