spacebox/src/cloud.cpp
Cocktail Frank 82202ce56a Add interface for reading and writing remote leaderboards database
Add client code for adding and reading remote leaderboard scores.

Combine leaderboards API server functions into one file.

Add case to test program with a mock database and mock security credentials
for testing remote leaderboard connection. Add a compilation flag to
indicate that PHP is available to test the API connection.
2025-10-19 18:59:14 -04:00

1045 lines
37 KiB
C++

#include "cloud.hpp"
namespace
{
bool _http_initialized { false };
}
void sb::cloud::init([[maybe_unused]] float steam_callback_frequency, [[maybe_unused]] bool initialize_steam)
{
/* Emscripten doesn't need to initialize the HTTP library. Other builds need to initialize cURL. */
#if defined(__CURL__)
/* Initialize cURL and store result. Initialize the multi request handler. */
CURLcode result { curl_global_init(CURL_GLOBAL_DEFAULT) };
if (result != CURLE_OK)
{
_http_initialized = false;
sb::Log::Multi(sb::Log::WARN) << "cURL failed to initialize and will not be available " <<
curl_easy_strerror(result) << sb::Log::end;
}
else
{
_http_initialized = true;
}
#else
_http_initialized = true;
#endif
/* Android builds should try to copy in CA certificates for SSL requests because these are not provided by
* Android. This is untested code that was copied over from another project. The CURLSSLOPT_NATIVE_CA option may
* be available on Android now, see https://curl.se/libcurl/c/CURLOPT_SSL_OPTIONS.html */
#if defined(__ANDROID__) || defined(ANDROID)
/* Copy CA bundle from the APK assets folder to internal storage. */
if (SDL_AndroidGetInternalStoragePath() != nullptr)
{
/* Copy the certificates into the internal storage. If successfully copied, save the path. This needs to be
* updated to use a parameter passed in directly instead of from the configuration. */
// ca_bundle_path = sb::copy_file(
// configuration()["scan"]["certificate-authorities-file"], SDL_AndroidGetInternalStoragePath())
if (!fs::exists(ca_bundle_path))
{
sb::Log::Line() << "Could not copy certificate authorities file, SSL peer verification will be disabled.";
}
}
else
{
sb::Log::sdl_error("Could not access Android internal storage, SSL peer verification will be disabled.");
}
#endif
#if defined(STEAM_ENABLED)
if (initialize_steam)
{
/* Initialize Steam */
steam::init(steam_callback_frequency);
}
#endif
}
void sb::cloud::quit()
{
_http_initialized = false;
/* Quit cURL */
#if defined(__CURL__)
curl_global_cleanup();
#endif
/* Quit Steam */
#if defined(STEAM_ENABLED)
steam::shutdown();
#endif
}
/* The entire HTTP library is unavailable if HTTP is not enabled. */
#if defined(HTTP_ENABLED)
sb::cloud::HTTP::HTTP()
{
#if defined(__CURL__)
if (initialized())
{
/* Initialize a new multi handle for this object */
curl_multi_handle.reset(curl_multi_init());
}
#endif
}
bool sb::cloud::HTTP::initialized()
{
return _http_initialized;
}
void sb::cloud::HTTP::get(const Request& request)
{
launch(request, "GET");
}
void sb::cloud::HTTP::post(const Request& request)
{
launch(request, "POST");
}
void sb::cloud::HTTP::launch(const HTTP::Request& original, const std::string& method)
{
/* Copy request into a new object. The pointer will be stored by the requests vector, guaranteeing its lifetime to
* last until the requests are processed and deleted. */
Request* request { new Request(original) };
requests.push_back(request);
/* Headers will be copied into the underlying HTTP request object for the Fetch API or cURL */
std::vector<std::string> headers = request->headers();
/* POST data must be a string which will able to be decoded as application/json */
if (method == "POST")
{
/* Add Content-Type to header */
headers.push_back("Content-Type");
headers.push_back("application/json");
}
/* Add authentication */
if (!request->authorization().empty())
{
headers.push_back("Authorization");
headers.push_back("Basic " + request->authorization());
}
sb::Log::Multi(sb::Log::DEBUG) << "Fetching data from " << request->url() << sb::Log::end;
/* Emscripten uses its built in Fetch API */
#if defined(__EMSCRIPTEN__)
/* Create a fetch attributes object. Set callbacks for when response is received. The callbacks will get the request
* pointer, so they can call the request's own callback. */
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, method.c_str());
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = fetch_success;
attr.onerror = fetch_error;
attr.userData = request;
attr.timeoutMSecs = int(request->timeout() * 1'000);
/* Add POST data */
if (method == "POST")
{
attr.requestData = request->data().data();
attr.requestDataSize = request->data().size() * sizeof(unsigned char);
}
/* Copy headers into a vector of C strings with null terminator for Emscripten */
if (!headers.empty())
{
std::vector<const char*>* emscripten_formatted_headers = new std::vector<const char*>();
for (const std::string& component : headers)
{
const std::string* component_c = new std::string(component.c_str());
emscripten_formatted_headers->push_back(component_c->c_str());
}
emscripten_formatted_headers->push_back(nullptr);
attr.requestHeaders = emscripten_formatted_headers->data();
/* Print the headers to the debug log */
std::ostringstream message;
message << "Sending headers to " << request->url() << ": ";
for (const char* component : *emscripten_formatted_headers)
{
if (component == nullptr)
{
break;
}
else
{
message << " " << component;
}
}
sb::Log::Line(sb::Log::DEBUG) << message.str();
}
emscripten_fetch(&attr, request->url().c_str());
#else
if (initialized())
{
CURL* curl_easy_handle = curl_easy_init();
if (curl_easy_handle)
{
curl_easy_setopt(curl_easy_handle, CURLOPT_URL, request->url().c_str());
curl_easy_setopt(curl_easy_handle, CURLOPT_WRITEFUNCTION, curl_write_response);
curl_easy_setopt(curl_easy_handle, CURLOPT_TIMEOUT_MS, int(request->timeout() * 1'000));
#if defined(_WIN32)
/* Use the operating system's native CA store for certificate validation. This option seems to be necessary
* on Windows. It shouldn't be problematic on other builds, but it is not available in older versions of
* cURL, which are still being used in some cases. This may work on Android but hasn't been tested yet. See
* https://curl.se/libcurl/c/CURLOPT_SSL_OPTIONS.html */
curl_easy_setopt(curl_easy_handle, CURLOPT_SSL_OPTIONS, static_cast<long>(CURLSSLOPT_NATIVE_CA));
#endif
/* WRITEDATA is set to the request object, so the storage in the request object can receive the transfer
* data */
CURLcode result { curl_easy_setopt(curl_easy_handle, CURLOPT_WRITEDATA, reinterpret_cast<void*>(request)) };
if (result != CURLE_OK)
{
sb::Log::Multi(sb::Log::WARN) << "Setting cURL handle's CURLOPT_WRITEDATA failed: " <<
curl_easy_strerror(result) << sb::Log::end;
}
/* PRIVATE is also set to the request object, so the request can call the response function when the multi
* handle has determined that all the transfer packets are complete */
result = curl_easy_setopt(curl_easy_handle, CURLOPT_PRIVATE, reinterpret_cast<void*>(request));
if (result != CURLE_OK)
{
sb::Log::Multi(sb::Log::WARN) << "Setting cURL handle's CURLOPT_PRIVATE failed: " <<
curl_easy_strerror(result) << sb::Log::end;
}
/* Add POST data */
if (method == "POST")
{
curl_easy_setopt(curl_easy_handle, CURLOPT_POSTFIELDS, request->data().c_str());
}
/* Pass submitted headers to cURL */
struct curl_slist* list { nullptr };
if (!headers.empty())
{
/* cURL expects headers as a list of "name: value" pair strings, so combine every two components of the
* headers list into a single string */
for (std::size_t ii = 0; ii < headers.size(); ii += 2)
{
std::string pair = headers[ii] + ": " + headers[ii + 1];
list = curl_slist_append(list, pair.c_str());
}
}
curl_easy_setopt(curl_easy_handle, CURLOPT_HTTPHEADER, list);
/* Add error string buffer */
request->attach_error_buffer(curl_easy_handle);
/* Android builds are untested, this code was copied over from a previous project */
#if defined(__ANDROID__) || defined(ANDROID)
/* CA certificates bundle path must be specified in Android. */
CURLcode res;
bool success { false };
std::ostringstream message;
if (fs::exists(ca_bundle_path))
{
res = curl_easy_setopt(curl_easy_handle, CURLOPT_CAINFO, ca_bundle_path.c_str());
if (res == CURLE_OK)
{
sb::Log::Multi(sb::Log::DEBUG) << "Set curl handle's CURLOPT_CAINFO to " << ca_bundle_path <<
sb::Log::end;
success = true;
}
else
{
message << "Setting curl handle's CURLOPT_CAINFO failed " << curl_easy_strerror(res);
}
}
/* Fallback to turning off SSL peer verification if the certificates file isn't available. */
if (!success)
{
sb::Log::Line(sb::Log::WARN) << "Turning off SSL peer verification";
curl_easy_setopt(curl_easy_handle, CURLOPT_SSL_VERIFYPEER, 0);
}
#endif
/* Add the easy handle to the multi handle, so it can be processed asynchronously in the update loop */
curl_multi_add_handle(curl_multi_handle.get(), curl_easy_handle);
}
else
{
sb::Log::Multi(sb::Log::DEBUG) << "cURL request initialization failed for " << request->url() <<
sb::Log::end;
}
}
#endif
}
#if defined(__EMSCRIPTEN__)
void sb::cloud::HTTP::fetch_success(emscripten_fetch_t* fetch)
{
sb::Log::Multi(sb::Log::DEBUG) << "Found " << fetch->numBytes << " bytes using Emscripten Fetch API" <<
sb::Log::end;
/* Store the bytes in the request object */
HTTP::Request* request { reinterpret_cast<Request*>(fetch->userData) };
request->store(reinterpret_cast<const std::uint8_t*>(fetch->data), fetch->numBytes);
/* Set status code to success and call the user supplied callback */
request->status(fetch->status);
if (request->callback() != nullptr)
{
request->callback()(*request);
}
request->complete(true);
emscripten_fetch_close(fetch);
}
void sb::cloud::HTTP::fetch_error(emscripten_fetch_t* fetch)
{
sb::Log::Multi(sb::Log::DEBUG) << "Failed fetching " << fetch->url << " with status code " << fetch->status <<
sb::Log::end;
/* Set failed status code and call the user supplied callback */
Request* request = reinterpret_cast<Request*>(fetch->userData);
request->status(fetch->status);
if (request->callback() != nullptr)
{
request->callback()(*request);
}
request->complete(true);
emscripten_fetch_close(fetch);
}
#else
std::size_t sb::cloud::HTTP::curl_write_response(
std::uint8_t* buffer, std::size_t size, std::size_t count, void* request_void)
{
std::size_t packet_size { size * count };
sb::Log::Multi(sb::Log::DEBUG) << "Found " << packet_size << " bytes using cURL " << sb::Log::end;
/* Store the bytes in the request object */
Request* request { reinterpret_cast<Request*>(request_void) };
request->store(buffer, packet_size);
return packet_size;
}
#endif
void sb::cloud::HTTP::update([[maybe_unused]] float timestamp)
{
if (initialized())
{
#if !defined(__EMSCRIPTEN__)
/* Calling multi perform will perform transfers on handles cURL has determined are ready for transfer. Any
* handles that have transferred all their necessary data will cause a CURLMSG_DONE message to be in the message
* queue. If a handle is done, call the request callback that has been attached to it, remove the handle, and
* clean it up. See https://curl.se/libcurl/c/curl_multi_info_read.html */
int running_handles { 0 };
CURLMcode multi_result { curl_multi_perform(curl_multi_handle.get(), &running_handles) };
if (multi_result != CURLM_OK)
{
sb::Log::Multi(sb::Log::WARN) << "Error while running curl_multi_perform: error code #" << multi_result <<
sb::Log::end;
}
struct CURLMsg* message_queue { nullptr };
do
{
int message_count { 0 };
message_queue = curl_multi_info_read(curl_multi_handle.get(), &message_count);
if (message_queue != nullptr && (message_queue->msg == CURLMSG_DONE))
{
/* Get request object from the curl handle */
void* request_void { nullptr };
CURLcode result { curl_easy_getinfo(message_queue->easy_handle, CURLINFO_PRIVATE, &request_void) };
if (result == CURLE_OK)
{
Request* request { reinterpret_cast<Request*>(request_void) };
/* Store the status in the request object */
long status { 0 };
curl_easy_getinfo(message_queue->easy_handle, CURLINFO_RESPONSE_CODE, &status);
request->status(status);
/* Print a debug message */
std::ostringstream message;
message << request->url() << " returned HTTP response " << status;
if (!request->error().empty())
{
message << ": " << request->error();
}
sb::Log::Line(sb::Log::DEBUG) << message.str();
/* The request finished, call the callback and mark complete. */
if (request->callback() != nullptr)
{
request->callback()(*request);
}
request->complete(true);
}
else
{
sb::Log::Multi(sb::Log::WARN) << "Unable to retrieve result object from cURL handle: " <<
curl_easy_strerror(result) << sb::Log::end;
}
/* Clean up the cURL handle */
curl_multi_remove_handle(curl_multi_handle.get(), message_queue->easy_handle);
curl_easy_cleanup(message_queue->easy_handle);
}
} while(message_queue != nullptr);
#endif
}
/* Delete and erase finished requests from the vector using iterators to erase while reading the vector */
for (auto iter = requests.begin(); iter != requests.end();)
{
if ((*iter)->complete())
{
sb::Log::Multi(sb::Log::DEBUG) << "Freeing and removing request object for " << (*iter)->url() <<
sb::Log::end;
/* Free the heap allocated Request */
delete *iter;
/* Get a iterator that points to the next request, which may have been moved after erase */
iter = requests.erase(iter);
}
else
{
/* Only increment the iterator when there was no erase */
iter++;
}
}
}
std::size_t sb::cloud::HTTP::count() const
{
return requests.size();
}
sb::cloud::HTTP::Request& sb::cloud::HTTP::queue(std::size_t index)
{
try
{
return *requests.at(index);
}
catch (const std::out_of_range& error)
{
std::ostringstream message;
message << "Request #" << index << " does not exist in the queue";
throw std::out_of_range(message.str());
}
}
void sb::cloud::HTTP::post_analytics(
const std::string& url,
nlohmann::json analytics,
const std::string& game_title,
const std::string& game_version,
const std::string& platform,
const std::string& save_data_id,
const std::string& authorization,
const std::vector<std::string>& headers)
{
if (initialized())
{
/* Add engine information */
analytics["engine version"] = sb::version;
analytics["build"] = sb::build;
/* Add game information. If the session already contains a value for a field, only overwrite that value if the
* incoming value is not empty. */
if (!game_version.empty() || analytics.value("game version", "").empty())
{
analytics["game version"] = game_version;
}
if (!game_title.empty() || analytics.value("game title", "").empty())
{
analytics["game title"] = game_title;
}
if (!platform.empty() || analytics.value("platform", "").empty())
{
analytics["platform"] = platform;
}
if (!save_data_id.empty() || analytics.value("save data id", "").empty())
{
analytics["save data id"] = save_data_id;
}
#if defined(STEAM_ENABLED)
if (steam::initialized())
{
/* Hash the Steam user name, so the player cannot be identified */
SHA256 sha256;
std::string handle { SteamAPI_ISteamFriends_GetPersonaName(SteamFriends()) };
std::string hash { sha256.hash(handle) };
analytics["steam user"] = hash.substr(0, 6);
/* If the platform is unspecified, and the Steam API is connected, it can be assumed the platform is
* Steam. */
if (analytics.value("platform", "").empty() && platform.empty())
{
analytics["platform"] = "steam";
}
}
#endif
/* Send the session data to the remote logger */
Request request { url };
request.data(analytics);
request.authorization(authorization);
request.headers(headers);
request.callback([&](const Request& request){
if (request.status() == 200)
{
sb::Log::Multi(sb::Log::DEBUG) << "Successfully posted session data to " << request.url() <<
sb::Log::end;
}
else
{
sb::Log::Multi(sb::Log::DEBUG) << "Error posting session data to " << request.url() << ". " <<
request.status() << " " << request.error() << sb::Log::end;
}
});
post(request);
}
}
sb::cloud::HTTP::Request::Request(const std::string& url) : _url(url) {}
void sb::cloud::HTTP::Request::headers(const std::vector<std::string>& headers)
{
_headers = headers;
}
const std::vector<std::string>& sb::cloud::HTTP::Request::headers() const
{
return _headers;
}
const std::string& sb::cloud::HTTP::Request::url() const
{
return _url;
}
void sb::cloud::HTTP::Request::timeout(float seconds)
{
_timeout = seconds;
}
float sb::cloud::HTTP::Request::timeout() const
{
return _timeout;
}
void sb::cloud::HTTP::Request::data(const nlohmann::json& json)
{
_data = json.dump();
}
const std::string& sb::cloud::HTTP::Request::data() const
{
return _data;
}
void sb::cloud::HTTP::Request::authorization(const std::string& key)
{
_authorization = key;
}
const std::string& sb::cloud::HTTP::Request::authorization() const
{
return _authorization;
}
void sb::cloud::HTTP::Request::callback(Callback callback)
{
_callback = callback;
}
sb::cloud::HTTP::Request::Callback sb::cloud::HTTP::Request::callback()
{
return _callback;
}
void sb::cloud::HTTP::Request::store(const std::uint8_t* buffer, const std::size_t& size)
{
response.insert(response.end(), buffer, buffer + size);
sb::Log::Multi(sb::Log::DEBUG) << "Request " << url() << " has stored " << (response.size() / 100.0f) << "KB" <<
sb::Log::end;
}
int sb::cloud::HTTP::Request::status() const
{
return _status;
}
void sb::cloud::HTTP::Request::status(int status)
{
_status = status;
}
bool sb::cloud::HTTP::Request::complete() const
{
return _complete;
}
void sb::cloud::HTTP::Request::complete(bool complete)
{
_complete = complete;
}
std::string sb::cloud::HTTP::Request::error() const
{
#if !defined(__EMSCRIPTEN__)
return std::string(_error);
#else
return "";
#endif
}
#if !defined(__EMSCRIPTEN__)
void sb::cloud::HTTP::Request::attach_error_buffer(CURL* handle)
{
_error[0] = '\0';
curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, _error);
}
#endif
const std::vector<std::uint8_t>& sb::cloud::HTTP::Request::bytes() const
{
return response;
}
nlohmann::json sb::cloud::HTTP::Request::json() const
{
try
{
return nlohmann::json::parse(response);
}
catch (const nlohmann::json::parse_error& error)
{
sb::Log::Multi(sb::Log::WARN) << "Invalid JSON in response from " << url();
return nlohmann::json();
}
}
#endif
#if defined(STEAM_ENABLED)
/*!
* Because this is an unnamed namespace, the properties are not accessible outside of this file, even if this
* header is included in another file.
*/
namespace
{
sb::Animation dispatch;
bool _steam_initialized = false;
bool _overlay_active = false;
std::optional<int> _player_count;
std::uint32_t app_id;
bool _global_stats_available = false;
}
HSteamPipe steam_pipe;
void sb::cloud::steam::count_players()
{
if (initialized())
{
/* This returns a SteamAPICall_t, but it's not needed. */
SteamAPI_ISteamUserStats_GetNumberOfCurrentPlayers(SteamUserStats());
}
}
std::optional<int> sb::cloud::steam::player_count()
{
return _player_count;
}
bool sb::cloud::steam::overlay_active()
{
return _overlay_active;
}
void sb::cloud::steam::init(float callback_frequency)
{
SteamErrMsg error;
if (SteamAPI_InitFlat(&error) == k_ESteamAPIInitResult_OK)
{
SteamAPI_ManualDispatch_Init();
_steam_initialized = true;
app_id = SteamAPI_ISteamUtils_GetAppID(SteamUtils());
sb::Log::Multi() << "Initialized Steam API" << std::endl << "⮑ App ID " <<
app_id << std::endl << " Build ID " << SteamAPI_ISteamApps_GetAppBuildId(SteamApps()) << std::endl <<
" Current user ID is " << SteamAPI_ISteamUser_GetSteamID(SteamUser()) << std::endl <<
" Current user name is " << SteamAPI_ISteamFriends_GetPersonaName(SteamFriends()) << std::endl <<
" Is user logged in? " << std::boolalpha << SteamAPI_ISteamUser_BLoggedOn(SteamUser()) << sb::Log::end;
dispatch.frame_length(callback_frequency);
dispatch.play();
steam_pipe = SteamAPI_GetHSteamPipe();
SteamAPI_ISteamUserStats_RequestCurrentStats(SteamUserStats());
}
else
{
if (!SteamAPI_IsSteamRunning())
{
sb::Log::Line() << "No running Steam client detected, so could not connect to the Steam API.";
}
else
{
sb::Log::Line() << "Steam client was detected, but could not connect the app to the Steam API.";
}
}
}
bool sb::cloud::steam::initialized()
{
return _steam_initialized;
}
void sb::cloud::steam::update(float timestamp)
{
if (dispatch.update(timestamp) && initialized())
{
SteamAPI_ManualDispatch_RunFrame(steam_pipe);
/*!
* CallbackMsg_t contains the following properties.
*
* m_hSteamUser HSteamUser The user this callback is sent to.
* m_iCallback int The unique ID of the callback (for example, SteamAPICallCompleted_t::k_iCallback)
* m_pubParam uint8* The pointer to the callback data.
* m_cubParam int The size of m_pubParam.
*
* See https://partner.steamgames.com/doc/api/ISteamUser#CallbackMsg_t for full documentation.
*/
CallbackMsg_t callback;
/*!
* It's also possible to check the status of a callback using the SteamAPICall_t object returned by the
* caller function:
*
* bool failure;
* sb::Log::Multi() << "Is call completed? " << std::boolalpha <<
* SteamAPI_ISteamUtils_IsAPICallCompleted(SteamUtils(), steam_num_players_call, &failure) <<
* " Is failure? " << failure << sb::Log::end;
*/
/* Loop over all callbacks incoming from the Steam API */
while (SteamAPI_ManualDispatch_GetNextCallback(steam_pipe, &callback))
{
sb::Log::Multi(sb::Log::DEBUG) << "Received callback ID " << callback.m_iCallback << " from Steam" <<
sb::Log::end;
/* Special callback which contains another callback and results which need allocated memory. */
if (callback.m_iCallback == SteamAPICallCompleted_t::k_iCallback)
{
/*!
* SteamAPICallCompleted_t contains the following properties.
*
* m_hAsyncCall SteamAPICall_t The handle of the Steam API Call that completed
* m_iCallback int k_iCallback constant which uniquely identifies the completed callback
* m_cubParam uint32 The size in bytes of the completed callback
*
* See https://partner.steamgames.com/doc/api/ISteamUtils#SteamAPICallCompleted_t for full
* documentation.
*/
SteamAPICallCompleted_t* call_completed =
reinterpret_cast<SteamAPICallCompleted_t*>(callback.m_pubParam);
/* Allocate memory for storing result data (for example, the count of current players). */
void* temp_call_result = malloc(callback.m_cubParam);
/* Failure flag */
bool failed;
/* Fill the call result data pointer with results. */
if (SteamAPI_ManualDispatch_GetAPICallResult(
steam_pipe, call_completed->m_hAsyncCall, temp_call_result, callback.m_cubParam,
call_completed->m_iCallback, &failed))
{
sb::Log::Multi(sb::Log::DEBUG) << "Got callback ID " << call_completed->m_iCallback <<
" from call result from Steam" << sb::Log::end;
if (failed)
{
std::string failure_heading = "Steam API call failure: ";
std::string failure_message;
ESteamAPICallFailure failure = SteamAPI_ISteamUtils_GetAPICallFailureReason(
SteamUtils(), call_completed->m_hAsyncCall);
if (failure == k_ESteamAPICallFailureNone)
{
failure_message = "No Failure";
}
else if (failure == k_ESteamAPICallFailureSteamGone)
{
failure_message = "The local Steam process has stopped responding, it may "
"have been forcefully closed or is frozen.";
}
else if (failure == k_ESteamAPICallFailureNetworkFailure)
{
failure_message = "The network connection to the Steam servers has been "
"lost, or was already broken.";
}
else if (failure == k_ESteamAPICallFailureInvalidHandle)
{
failure_message = "The SteamAPICall_t handle passed in no longer exists.";
}
else if (failure == k_ESteamAPICallFailureMismatchedCallback)
{
failure_message = "GetAPICallResult was called with the wrong callback type for this API call.";
}
sb::Log::Multi(sb::Log::WARN) << failure_heading << failure_message << sb::Log::end;
}
else
{
if (call_completed->m_iCallback == NumberOfCurrentPlayers_t::k_iCallback)
{
/* Store player count */
_player_count = *(static_cast<int*>(temp_call_result));
sb::Log::Multi(sb::Log::DEBUG) << "Stored player count as " << _player_count.value() <<
sb::Log::end;
}
if (call_completed->m_iCallback == GlobalStatsReceived_t::k_iCallback)
{
GlobalStatsReceived_t* call_result {
reinterpret_cast<GlobalStatsReceived_t*>(temp_call_result) };
if (call_result->m_eResult == k_EResultOK)
{
sb::Log::Line(sb::Log::DEBUG) << "Request for global stats succeeded";
_global_stats_available = true;
}
else if (call_result->m_eResult == k_EResultInvalidState)
{
sb::Log::Multi(sb::Log::WARN) << "Current stats request must be completed before " <<
" request for global stats can complete" << sb::Log::end;
}
else
{
sb::Log::Multi(sb::Log::WARN) << "Request for global stats from Steam failed with " <<
"result " << call_result->m_eResult << sb::Log::end;
}
}
}
}
else
{
sb::Log::Multi(sb::Log::WARN) << "Failed to get API call result from Steam for callback ID " <<
call_completed->m_iCallback << sb::Log::end;
}
free(temp_call_result);
}
else if (callback.m_iCallback == GameOverlayActivated_t::k_iCallback)
{
/* Post a message that the Steam overlay was activated and store the state. */
GameOverlayActivated_t* overlay_activated = reinterpret_cast<GameOverlayActivated_t*>(
callback.m_pubParam);
/* Make sure the event is from this app */
if (app_id == overlay_activated->m_nAppID)
{
if (overlay_activated->m_bActive)
{
_overlay_active = true;
sb::Delegate::post("steam overlay activated");
}
else
{
_overlay_active = false;
sb::Delegate::post("steam overlay deactivated");
}
}
}
else if (callback.m_iCallback == UserStatsReceived_t::k_iCallback)
{
UserStatsReceived_t* user_stats_received = reinterpret_cast<UserStatsReceived_t*>(callback.m_pubParam);
/* Make sure the game ID matches the app ID */
if (app_id == user_stats_received->m_nGameID)
{
if (user_stats_received->m_eResult == k_EResultOK)
{
sb::Log::Line(sb::Log::DEBUG) << "Received user stats from Steam";
}
else
{
sb::Log::Multi(sb::Log::WARN) << "Failed to get user stats from Steam, error code " <<
user_stats_received->m_eResult << sb::Log::end;
}
}
}
else if (callback.m_iCallback == UserStatsStored_t::k_iCallback)
{
UserStatsStored_t* user_stats_stored = reinterpret_cast<UserStatsStored_t*>(callback.m_pubParam);
/* Only process events for this game */
if (app_id == user_stats_stored->m_nGameID)
{
if (user_stats_stored->m_eResult == k_EResultOK)
{
sb::Log::Line(sb::Log::DEBUG) << "Stored stats on Steam servers";
}
else
{
sb::Log::Multi(sb::Log::WARN) << "Failed to sync stats with Steam servers, error code " <<
user_stats_stored->m_eResult << sb::Log::end;
}
}
/* Request to sync with the updated data */
SteamAPI_ISteamUserStats_RequestCurrentStats(SteamUserStats());
}
else if (callback.m_iCallback == UserAchievementStored_t::k_iCallback)
{
UserAchievementStored_t* user_achievement_stored = reinterpret_cast<UserAchievementStored_t*>(
callback.m_pubParam);
/* Only process events for this game */
if (app_id == user_achievement_stored->m_nGameID)
{
sb::Log::Multi(sb::Log::DEBUG) << "Stored achievement " <<
user_achievement_stored->m_rgchAchievementName << " on Steam server (progress " <<
user_achievement_stored->m_nCurProgress << "/" << user_achievement_stored->m_nMaxProgress <<
")";
}
}
else if (callback.m_iCallback == SteamServersConnected_t::k_iCallback)
{
sb::Log::Line() << "Connected to Steam servers";
}
else if (callback.m_iCallback == SteamServersDisconnected_t::k_iCallback)
{
sb::Log::Line(sb::Log::WARN) << "Disconnected from Steam servers";
}
/* Free the memory allocated to the callback */
SteamAPI_ManualDispatch_FreeLastCallback(steam_pipe);
}
}
}
void sb::cloud::steam::store_stats()
{
if (initialized())
{
SteamAPI_ISteamUserStats_StoreStats(SteamUserStats());
}
}
void sb::cloud::steam::request_global_stats(int days)
{
if (initialized())
{
_global_stats_available = false;
SteamAPI_ISteamUserStats_RequestGlobalStats(SteamUserStats(), days);
}
}
bool sb::cloud::steam::global_stats_available()
{
return _global_stats_available;
}
bool sb::cloud::steam::get_global_stat_total(const sb::progress::Stat& stat, float& total)
{
if (global_stats_available())
{
bool success;
/* Handle INT64 and DOUBLE stats differently, but return as a float regardless of type. */
if (stat.type() == sb::progress::Stat::FLOAT)
{
double remote;
success = SteamAPI_ISteamUserStats_GetGlobalStatDouble(SteamUserStats(), stat.id().c_str(), &remote);
total = remote;
}
else
{
long long remote;
success = SteamAPI_ISteamUserStats_GetGlobalStatInt64(SteamUserStats(), stat.id().c_str(), &remote);
total = remote;
}
return success;
}
else
{
return false;
}
}
bool sb::cloud::steam::get_global_stat_history(
const sb::progress::Stat& stat, std::size_t days, std::vector<float>& history)
{
if (global_stats_available())
{
bool success;
/* Handle DOUBLE and INT64 stat types separately. Create a vector reserved to hold the requested number of days.
* Pass a pointer to the vector and the remote will fill the vector with the history data. */
if (stat.type() == sb::progress::Stat::FLOAT)
{
std::vector<double> remote(days, 0.0);
int response {
SteamAPI_ISteamUserStats_GetGlobalStatHistoryDouble(
SteamUserStats(), stat.id().c_str(), remote.data(), remote.capacity() * sizeof(double))
};
success = response > 0;
if (success)
{
for (double day : remote)
{
history.push_back(day);
}
}
}
else
{
std::vector<long long> remote(days, 0);
int response {
SteamAPI_ISteamUserStats_GetGlobalStatHistoryInt64(
SteamUserStats(), stat.id().c_str(), remote.data(), remote.capacity() * sizeof(long long))
};
success = response > 0;
if (success)
{
for (long long day : remote)
{
history.push_back(day);
}
}
}
return success;
}
else
{
return false;
}
}
void sb::cloud::steam::shutdown()
{
_steam_initialized = false;
if (initialized())
{
SteamAPI_Shutdown();
}
}
#endif