Fix #3355: Implement loading of parks from URLs

The help text for the command line options already referenced the
possibility of opening a saved park directly from a URL, but this was
not yet implemented. This commit changes all path handling for command
line options to accept both local paths and URLs.

If a URL is specified instead of a local path, the program will download
the file to the operating system's temp directory. It will then proceed
to load this file just like local files would be loaded.

The program will try to derive the extension of the temp file from the
original URL and defaults to sv6 (a save file) if it is unable to do so.
This commit is contained in:
Alexander Overvoorde 2016-10-30 03:15:27 +01:00 committed by Ted John
parent 729ac13537
commit 61f4452526
7 changed files with 1026 additions and 196 deletions

View File

@ -121,8 +121,13 @@ static void PrintLaunchInformation();
const CommandLineCommand CommandLine::RootCommands[]
{
// Main commands
#ifndef DISABLE_HTTP
DefineCommand("", "<uri>", StandardOptions, HandleNoCommand ),
DefineCommand("edit", "<uri>", StandardOptions, HandleCommandEdit ),
#else
DefineCommand("", "<path>", StandardOptions, HandleNoCommand ),
DefineCommand("edit", "<path>", StandardOptions, HandleCommandEdit ),
#endif
DefineCommand("intro", "", StandardOptions, HandleCommandIntro ),
#ifndef DISABLE_NETWORK
DefineCommand("host", "<uri>", StandardOptions, HandleCommandHost ),
@ -148,7 +153,9 @@ const CommandLineExample CommandLine::RootExamples[]
{ "./my_park.sv6", "open a saved park" },
{ "./SnowyPark.sc6", "install and open a scenario" },
{ "./ShuttleLoop.td6", "install a track" },
#ifndef DISABLE_HTTP
{ "https://openrct2.website/files/SnowyPark.sv6", "download and open a saved park" },
#endif
#ifndef DISABLE_NETWORK
{ "host ./my_park.sv6 --port 11753 --headless", "run a headless server for a saved park" },
#endif

View File

@ -100,7 +100,7 @@ private:
_lastAdvertiseTime = SDL_GetTicks();
// Send the registration request
http_json_request request;
http_request_t request;
request.tag = this;
request.url = GetMasterServerUrl();
request.method = HTTP_METHOD_POST;
@ -108,9 +108,10 @@ private:
json_t *body = json_object();
json_object_set_new(body, "key", json_string(_key.c_str()));
json_object_set_new(body, "port", json_integer(_port));
request.body = body;
request.root = body;
request.type = HTTP_DATA_JSON;
http_request_json_async(&request, [](http_json_response * response) -> void
http_request_async(&request, [](http_response_t * response) -> void
{
if (response == nullptr)
{
@ -120,7 +121,7 @@ private:
{
auto advertiser = static_cast<NetworkServerAdvertiser*>(response->tag);
advertiser->OnRegistrationResponse(response->root);
http_request_json_dispose(response);
http_request_dispose(response);
}
});
@ -129,16 +130,17 @@ private:
void SendHeartbeat()
{
http_json_request request;
http_request_t request;
request.tag = this;
request.url = GetMasterServerUrl();
request.method = HTTP_METHOD_PUT;
json_t * jsonBody = GetHeartbeatJson();
request.body = jsonBody;
request.root = jsonBody;
request.type = HTTP_DATA_JSON;
_lastHeartbeatTime = SDL_GetTicks();
http_request_json_async(&request, [](http_json_response *response) -> void
http_request_async(&request, [](http_response_t *response) -> void
{
if (response == nullptr)
{
@ -148,7 +150,7 @@ private:
{
auto advertiser = static_cast<NetworkServerAdvertiser*>(response->tag);
advertiser->OnHeartbeatResponse(response->root);
http_request_json_dispose(response);
http_request_dispose(response);
}
});

View File

@ -15,8 +15,8 @@
#pragma endregion
extern "C" {
#include "http.h"
#include "../platform/platform.h"
#include "http.h"
#include "../platform/platform.h"
}
#ifdef DISABLE_HTTP
@ -29,10 +29,11 @@ void http_dispose() { }
#include "../core/Math.hpp"
#include "../core/Path.hpp"
#include "../core/String.hpp"
#include "../core/Console.hpp"
#ifdef __WINDOWS__
// cURL includes windows.h, but we don't need all of it.
#define WIN32_LEAN_AND_MEAN
// cURL includes windows.h, but we don't need all of it.
#define WIN32_LEAN_AND_MEAN
#endif
#include <curl/curl.h>
@ -40,189 +41,290 @@ void http_dispose() { }
#define OPENRCT2_USER_AGENT "OpenRCT2/" OPENRCT2_VERSION
typedef struct read_buffer {
char *ptr;
size_t length;
size_t position;
char *ptr;
size_t length;
size_t position;
} read_buffer;
typedef struct write_buffer {
char *ptr;
size_t length;
size_t capacity;
char *ptr;
size_t length;
size_t capacity;
} write_buffer;
void http_init()
{
curl_global_init(CURL_GLOBAL_DEFAULT);
curl_global_init(CURL_GLOBAL_DEFAULT);
}
void http_dispose()
{
curl_global_cleanup();
curl_global_cleanup();
}
static size_t http_request_read_func(void *ptr, size_t size, size_t nmemb, void *userdata)
{
read_buffer *readBuffer = (read_buffer*)userdata;
read_buffer *readBuffer = (read_buffer*)userdata;
size_t remainingBytes = readBuffer->length - readBuffer->position;
size_t readBytes = size * nmemb;
if (readBytes > remainingBytes) {
readBytes = remainingBytes;
}
size_t remainingBytes = readBuffer->length - readBuffer->position;
size_t readBytes = size * nmemb;
if (readBytes > remainingBytes) {
readBytes = remainingBytes;
}
memcpy(ptr, readBuffer->ptr + readBuffer->position, readBytes);
memcpy(ptr, readBuffer->ptr + readBuffer->position, readBytes);
readBuffer->position += readBytes;
return readBytes;
readBuffer->position += readBytes;
return readBytes;
}
static size_t http_request_write_func(void *ptr, size_t size, size_t nmemb, void *userdata)
{
write_buffer *writeBuffer = (write_buffer*)userdata;
write_buffer *writeBuffer = (write_buffer*)userdata;
size_t newBytesLength = size * nmemb;
if (newBytesLength > 0) {
size_t newCapacity = writeBuffer->capacity;
size_t newLength = writeBuffer->length + newBytesLength;
while (newLength > newCapacity) {
newCapacity = Math::Max<size_t>(4096, newCapacity * 2);
size_t newBytesLength = size * nmemb;
if (newBytesLength > 0) {
size_t newCapacity = writeBuffer->capacity;
size_t newLength = writeBuffer->length + newBytesLength;
while (newLength > newCapacity) {
newCapacity = Math::Max<size_t>(4096, newCapacity * 2);
}
if (newCapacity != writeBuffer->capacity) {
writeBuffer->ptr = (char*)realloc(writeBuffer->ptr, newCapacity);
writeBuffer->capacity = newCapacity;
}
memcpy(writeBuffer->ptr + writeBuffer->length, ptr, newBytesLength);
writeBuffer->length = newLength;
}
return newBytesLength;
}
http_response_t *http_request(const http_request_t *request)
{
CURL *curl;
CURLcode curlResult;
http_response_t *response;
read_buffer readBuffer = { 0 };
write_buffer writeBuffer;
curl = curl_easy_init();
if (curl == NULL)
return NULL;
if (request->type == HTTP_DATA_JSON && request->root != NULL) {
readBuffer.ptr = json_dumps(request->root, JSON_COMPACT);
readBuffer.length = strlen(readBuffer.ptr);
readBuffer.position = 0;
} else if (request->type == HTTP_DATA_RAW && request->body != NULL) {
readBuffer.ptr = request->body;
readBuffer.length = request->size;
readBuffer.position = 0;
}
writeBuffer.ptr = NULL;
writeBuffer.length = 0;
writeBuffer.capacity = 0;
curl_slist *headers = NULL;
if (request->type == HTTP_DATA_JSON) {
headers = curl_slist_append(headers, "Accept: " MIME_TYPE_APPLICATION_JSON);
if (request->root != NULL) {
headers = curl_slist_append(headers, "Content-Type: " MIME_TYPE_APPLICATION_JSON);
}
}
if (readBuffer.ptr != NULL) {
char contentLengthHeaderValue[64];
snprintf(contentLengthHeaderValue, sizeof(contentLengthHeaderValue), "Content-Length: %zu", readBuffer.length);
headers = curl_slist_append(headers, contentLengthHeaderValue);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, readBuffer.ptr);
}
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request->method);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, true);
curl_easy_setopt(curl, CURLOPT_USERAGENT, OPENRCT2_USER_AGENT);
curl_easy_setopt(curl, CURLOPT_URL, request->url);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeBuffer);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_request_write_func);
curlResult = curl_easy_perform(curl);
if (request->type == HTTP_DATA_JSON && request->root != NULL) {
free(readBuffer.ptr);
}
if (curlResult != CURLE_OK) {
log_error("HTTP request failed: %s.", curl_easy_strerror(curlResult));
if (writeBuffer.ptr != NULL)
free(writeBuffer.ptr);
return NULL;
}
long httpStatusCode;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatusCode);
char* contentType;
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &contentType);
// Null terminate the response buffer
writeBuffer.length++;
writeBuffer.ptr = (char*)realloc(writeBuffer.ptr, writeBuffer.length);
writeBuffer.capacity = writeBuffer.length;
writeBuffer.ptr[writeBuffer.length - 1] = 0;
response = NULL;
// Parse as JSON if response is JSON
if (contentType != NULL && strstr(contentType, "json") != NULL) {
json_t *root;
json_error_t error;
root = json_loads(writeBuffer.ptr, 0, &error);
if (root != NULL) {
response = (http_response_t*) malloc(sizeof(http_response_t));
response->tag = request->tag;
response->status_code = (int) httpStatusCode;
response->root = root;
response->type = HTTP_DATA_JSON;
response->size = writeBuffer.length;
}
free(writeBuffer.ptr);
} else {
response = (http_response_t*) malloc(sizeof(http_response_t));
response->tag = request->tag;
response->status_code = (int) httpStatusCode;
response->body = writeBuffer.ptr;
response->type = HTTP_DATA_RAW;
response->size = writeBuffer.length;
}
curl_easy_cleanup(curl);
return response;
}
void http_request_async(const http_request_t *request, void (*callback)(http_response_t*))
{
struct TempThreadArgs {
http_request_t request;
void (*callback)(http_response_t*);
};
TempThreadArgs *args = (TempThreadArgs*)malloc(sizeof(TempThreadArgs));
args->request.url = _strdup(request->url);
args->request.method = request->method;
if (request->type == HTTP_DATA_JSON) {
args->request.root = json_deep_copy(request->root);
} else {
char* bodyCopy = (char*) malloc(request->size);
memcpy(bodyCopy, request->body, request->size);
args->request.body = bodyCopy;
}
args->request.type = request->type;
args->request.size = request->size;
args->request.tag = request->tag;
args->callback = callback;
SDL_Thread *thread = SDL_CreateThread([](void *ptr) -> int {
TempThreadArgs *args = (TempThreadArgs*)ptr;
http_response_t *response = http_request(&args->request);
args->callback(response);
free((char*)args->request.url);
if (args->request.type == HTTP_DATA_JSON) {
json_decref((json_t*) args->request.root);
} else {
free(args->request.body);
}
free(args);
return 0;
}, NULL, args);
if (thread == NULL) {
log_error("Unable to create thread!");
callback(NULL);
} else {
SDL_DetachThread(thread);
}
}
void http_request_dispose(http_response_t *response)
{
if (response->type == HTTP_DATA_JSON && response->root != NULL)
json_decref(response->root);
else if (response->type == HTTP_DATA_RAW && response->body != NULL)
free(response->body);
free(response);
}
const char *http_get_extension_from_url(const char *url, const char *fallback)
{
const char *extension = strrchr(url, '.');
// Assume a save file by default if no valid extension can be determined
if (extension == NULL || strchr(extension, '/') != NULL) {
return fallback;
} else {
return extension;
}
}
bool http_download_park(const char *url, char tmpPath[L_tmpnam + 10])
{
// Download park to buffer in memory
http_request_t request;
request.url = url;
request.method = "GET";
request.type = HTTP_DATA_NONE;
http_response_t *response = http_request(&request);
if (response == NULL || response->status_code != 200) {
Console::Error::WriteLine("Failed to download '%s'", request.url);
if (response != NULL) {
http_request_dispose(response);
}
if (newCapacity != writeBuffer->capacity) {
writeBuffer->ptr = (char*)realloc(writeBuffer->ptr, newCapacity);
writeBuffer->capacity = newCapacity;
}
memcpy(writeBuffer->ptr + writeBuffer->length, ptr, newBytesLength);
writeBuffer->length = newLength;
}
return newBytesLength;
}
http_json_response *http_request_json(const http_json_request *request)
{
CURL *curl;
CURLcode curlResult;
http_json_response *response;
read_buffer readBuffer = { 0 };
write_buffer writeBuffer;
curl = curl_easy_init();
if (curl == NULL)
return NULL;
if (request->body != NULL) {
readBuffer.ptr = json_dumps(request->body, JSON_COMPACT);
readBuffer.length = strlen(readBuffer.ptr);
readBuffer.position = 0;
return false;
}
writeBuffer.ptr = NULL;
writeBuffer.length = 0;
writeBuffer.capacity = 0;
// Generate temporary filename that includes the original extension
if (tmpnam(tmpPath) == NULL) {
Console::Error::WriteLine("Failed to generate temporary filename for downloaded park '%s'", request.url);
http_request_dispose(response);
return false;
}
size_t remainingBytes = L_tmpnam + 10 - strlen(tmpPath);
curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Accept: " MIME_TYPE_APPLICATION_JSON);
if (request->body != NULL) {
headers = curl_slist_append(headers, "Content-Type: " MIME_TYPE_APPLICATION_JSON);
const char *ext = http_get_extension_from_url(request.url, ".sv6");
strncat(tmpPath, ext, remainingBytes);
char contentLengthHeaderValue[64];
snprintf(contentLengthHeaderValue, sizeof(contentLengthHeaderValue), "Content-Length: %zu", readBuffer.length);
headers = curl_slist_append(headers, contentLengthHeaderValue);
// Store park in temporary file and load it (discard ending NUL in response body)
FILE* tmpFile = fopen(tmpPath, "wb");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, readBuffer.ptr);
if (tmpFile == NULL) {
Console::Error::WriteLine("Failed to write downloaded park '%s' to temporary file", request.url);
http_request_dispose(response);
return false;
}
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request->method);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, true);
curl_easy_setopt(curl, CURLOPT_USERAGENT, OPENRCT2_USER_AGENT);
curl_easy_setopt(curl, CURLOPT_URL, request->url);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeBuffer);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_request_write_func);
fwrite(response->body, 1, response->size - 1, tmpFile);
fclose(tmpFile);
curlResult = curl_easy_perform(curl);
http_request_dispose(response);
if (request->body != NULL) {
free(readBuffer.ptr);
}
if (curlResult != CURLE_OK) {
log_error("HTTP request failed: %s.", curl_easy_strerror(curlResult));
if (writeBuffer.ptr != NULL)
free(writeBuffer.ptr);
return NULL;
}
long httpStatusCode;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatusCode);
curl_easy_cleanup(curl);
// Null terminate the response buffer
writeBuffer.length++;
writeBuffer.ptr = (char*)realloc(writeBuffer.ptr, writeBuffer.length);
writeBuffer.capacity = writeBuffer.length;
writeBuffer.ptr[writeBuffer.length - 1] = 0;
response = NULL;
// Parse as JSON
json_t *root;
json_error_t error;
root = json_loads(writeBuffer.ptr, 0, &error);
if (root != NULL) {
response = (http_json_response*)malloc(sizeof(http_json_response));
response->tag = request->tag;
response->status_code = (int)httpStatusCode;
response->root = root;
}
free(writeBuffer.ptr);
return response;
}
void http_request_json_async(const http_json_request *request, void (*callback)(http_json_response*))
{
struct TempThreadArgs {
http_json_request request;
void (*callback)(http_json_response*);
};
TempThreadArgs *args = (TempThreadArgs*)malloc(sizeof(TempThreadArgs));
args->request.url = _strdup(request->url);
args->request.method = request->method;
args->request.body = json_deep_copy(request->body);
args->request.tag = request->tag;
args->callback = callback;
SDL_Thread *thread = SDL_CreateThread([](void *ptr) -> int {
TempThreadArgs *args = (TempThreadArgs*)ptr;
http_json_response *response = http_request_json(&args->request);
args->callback(response);
free((char*)args->request.url);
json_decref((json_t*)args->request.body);
free(args);
return 0;
}, NULL, args);
if (thread == NULL) {
log_error("Unable to create thread!");
callback(NULL);
} else {
SDL_DetachThread(thread);
}
}
void http_request_json_dispose(http_json_response *response)
{
if (response->root != NULL)
json_decref(response->root);
free(response);
return true;
}
#endif

View File

@ -21,27 +21,48 @@
#include <jansson.h>
#include "../common.h"
typedef struct http_json_request {
typedef enum http_data_type_T {
HTTP_DATA_NONE,
HTTP_DATA_RAW,
HTTP_DATA_JSON
} http_data_type;
typedef struct http_request_t {
void *tag;
const char *method;
const char *url;
const json_t *body;
} http_json_request;
http_data_type type;
size_t size;
union {
const json_t *root;
char* body;
};
} http_request_t;
typedef struct http_json_response {
typedef struct http_response_t {
void *tag;
int status_code;
json_t *root;
} http_json_response;
http_data_type type;
size_t size;
union {
json_t *root;
char* body;
};
} http_response_t;
#define HTTP_METHOD_GET "GET"
#define HTTP_METHOD_POST "POST"
#define HTTP_METHOD_PUT "PUT"
#define HTTP_METHOD_DELETE "DELETE"
http_json_response *http_request_json(const http_json_request *request);
void http_request_json_async(const http_json_request *request, void (*callback)(http_json_response*));
void http_request_json_dispose(http_json_response *response);
http_response_t *http_request(const http_request_t *request);
void http_request_async(const http_request_t *request, void (*callback)(http_response_t*));
void http_request_dispose(http_response_t *response);
const char *http_get_extension_from_url(const char *url, const char *fallback);
// Padding for extension that is appended to temporary file name
bool http_download_park(const char *url, char tmpPath[L_tmpnam + 10]);
#endif // DISABLE_HTTP
// These callbacks are defined anyway, but are dummy if HTTP is disabled

View File

@ -106,11 +106,11 @@ namespace Twitch
constexpr uint32 PulseTime = 10 * 1000;
constexpr const char * TwitchExtendedBaseUrl = "http://openrct.ursalabs.co/api/1/";
static int _twitchState = TWITCH_STATE_LEFT;
static bool _twitchIdle = true;
static uint32 _twitchLastPulseTick = 0;
static int _twitchLastPulseOperation = 1;
static http_json_response * _twitchJsonResponse;
static int _twitchState = TWITCH_STATE_LEFT;
static bool _twitchIdle = true;
static uint32 _twitchLastPulseTick = 0;
static int _twitchLastPulseOperation = 1;
static http_response_t * _twitchJsonResponse;
static void Join();
static void Leave();
@ -199,11 +199,12 @@ namespace Twitch
_twitchState = TWITCH_STATE_JOINING;
_twitchIdle = false;
http_json_request request;
http_request_t request;
request.url = url;
request.method = HTTP_METHOD_GET;
request.body = nullptr;
http_request_json_async(&request, [](http_json_response *jsonResponse) -> void
request.type = HTTP_DATA_JSON;
http_request_async(&request, [](http_response_t *jsonResponse) -> void
{
if (jsonResponse == nullptr)
{
@ -222,7 +223,7 @@ namespace Twitch
_twitchState = TWITCH_STATE_LEFT;
}
http_request_json_dispose(jsonResponse);
http_request_dispose(jsonResponse);
_twitchLastPulseTick = 0;
console_writeline("Connected to twitch channel.");
@ -238,7 +239,7 @@ namespace Twitch
{
if (_twitchJsonResponse != nullptr)
{
http_request_json_dispose(_twitchJsonResponse);
http_request_dispose(_twitchJsonResponse);
_twitchJsonResponse = nullptr;
}
@ -275,11 +276,12 @@ namespace Twitch
_twitchState = TWITCH_STATE_WAITING;
_twitchIdle = false;
http_json_request request;
http_request_t request;
request.url = url;
request.method = HTTP_METHOD_GET;
request.body = NULL;
http_request_json_async(&request, [](http_json_response * jsonResponse) -> void
request.type = HTTP_DATA_JSON;
http_request_async(&request, [](http_response_t * jsonResponse) -> void
{
if (jsonResponse == nullptr)
{
@ -305,11 +307,12 @@ namespace Twitch
_twitchState = TWITCH_STATE_WAITING;
_twitchIdle = false;
http_json_request request;
http_request_t request;
request.url = url;
request.method = HTTP_METHOD_GET;
request.body = nullptr;
http_request_json_async(&request, [](http_json_response * jsonResponse) -> void
request.type = HTTP_DATA_JSON;
http_request_async(&request, [](http_response_t * jsonResponse) -> void
{
if (jsonResponse == nullptr)
{
@ -326,7 +329,7 @@ namespace Twitch
static void ParseFollowers()
{
http_json_response *jsonResponse = _twitchJsonResponse;
http_response_t *jsonResponse = _twitchJsonResponse;
if (json_is_array(jsonResponse->root))
{
std::vector<AudienceMember> members;
@ -349,7 +352,7 @@ namespace Twitch
ManageGuestNames(members);
}
http_request_json_dispose(_twitchJsonResponse);
http_request_dispose(_twitchJsonResponse);
_twitchJsonResponse = NULL;
_twitchState = TWITCH_STATE_JOINED;
@ -358,7 +361,7 @@ namespace Twitch
static void ParseMessages()
{
http_json_response * jsonResponse = _twitchJsonResponse;
http_response_t * jsonResponse = _twitchJsonResponse;
if (json_is_array(jsonResponse->root))
{
size_t messageCount = json_array_size(jsonResponse->root);
@ -375,7 +378,7 @@ namespace Twitch
}
}
http_request_json_dispose(_twitchJsonResponse);
http_request_dispose(_twitchJsonResponse);
_twitchJsonResponse = nullptr;
_twitchState = TWITCH_STATE_JOINED;
}

694
src/openrct2.c Normal file
View File

@ -0,0 +1,694 @@
#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers
/*****************************************************************************
* OpenRCT2, an open source clone of Roller Coaster Tycoon 2.
*
* OpenRCT2 is the work of many authors, a full list can be found in contributors.md
* For more information, visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* A full copy of the GNU General Public License can be found in licence.txt
*****************************************************************************/
#pragma endregion
#include "audio/audio.h"
#include "audio/mixer.h"
#include "config.h"
#include "editor.h"
#include "game.h"
#include "hook.h"
#include "interface/chat.h"
#include "interface/themes.h"
#include "interface/window.h"
#include "interface/viewport.h"
#include "intro.h"
#include "localisation/localisation.h"
#include "network/http.h"
#include "network/network.h"
#include "object_list.h"
#include "openrct2.h"
#include "platform/crash.h"
#include "platform/platform.h"
#include "ride/ride.h"
#include "title.h"
#include "util/sawyercoding.h"
#include "util/util.h"
#include "version.h"
#include "world/mapgen.h"
#if defined(__unix__) || defined(__MACOSX__)
#include <sys/mman.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#endif // defined(__unix__) || defined(__MACOSX__)
int gExitCode;
#if defined(USE_MMAP) && (defined(__unix__) || defined(__MACOSX__)) && !defined(NO_RCT2)
static int fdData = -1;
#endif
#if defined(__unix__) && !defined(NO_RCT2)
static char * segments = (char *)(GOOD_PLACE_FOR_DATA_SEGMENT);
#endif
int gOpenRCT2StartupAction = STARTUP_ACTION_TITLE;
utf8 gOpenRCT2StartupActionPath[512] = { 0 };
utf8 gExePath[MAX_PATH];
utf8 gCustomUserDataPath[MAX_PATH] = { 0 };
utf8 gCustomOpenrctDataPath[MAX_PATH] = { 0 };
utf8 gCustomRCT2DataPath[MAX_PATH] = { 0 };
utf8 gCustomPassword[MAX_PATH] = { 0 };
// This should probably be changed later and allow a custom selection of things to initialise like SDL_INIT
bool gOpenRCT2Headless = false;
bool gOpenRCT2ShowChangelog;
bool gOpenRCT2SilentBreakpad;
#ifndef DISABLE_NETWORK
// OpenSSL's message digest context used for calculating sprite checksums
EVP_MD_CTX *gHashCTX = NULL;
#endif // DISABLE_NETWORK
/** If set, will end the OpenRCT2 game loop. Intentially private to this module so that the flag can not be set back to 0. */
int _finished;
// Used for object movement tweening
static rct_xyz16 _spritelocations1[MAX_SPRITES], _spritelocations2[MAX_SPRITES];
static void openrct2_loop();
static void openrct2_setup_rct2_hooks();
void openrct2_write_full_version_info(utf8 *buffer, size_t bufferSize)
{
utf8 *ch = buffer;
// Name and version
safe_strcpy(ch, OPENRCT2_NAME ", v" OPENRCT2_VERSION, bufferSize - (ch - buffer));
ch = strchr(ch, '\0');
// Build information
if (!str_is_null_or_empty(gGitBranch)) {
snprintf(ch, bufferSize - (ch - buffer), "-%s", gGitBranch);
ch = strchr(ch, '\0');
}
if (!str_is_null_or_empty(gCommitSha1Short)) {
snprintf(ch, bufferSize - (ch - buffer), " build %s", gCommitSha1Short);
ch = strchr(ch, '\0');
}
if (!str_is_null_or_empty(gBuildServer)) {
snprintf(ch, bufferSize - (ch - buffer), " provided by %s", gBuildServer);
ch = strchr(ch, '\0');
}
#if DEBUG
snprintf(ch, bufferSize - (ch - buffer), " (DEBUG)");
#endif
}
static void openrct2_copy_files_over(const utf8 *originalDirectory, const utf8 *newDirectory, const utf8 *extension)
{
utf8 *ch, filter[MAX_PATH], oldPath[MAX_PATH], newPath[MAX_PATH];
int fileEnumHandle;
file_info fileInfo;
if (!platform_ensure_directory_exists(newDirectory)) {
log_error("Could not create directory %s.", newDirectory);
return;
}
// Create filter path
safe_strcpy(filter, originalDirectory, sizeof(filter));
ch = strchr(filter, '*');
if (ch != NULL)
*ch = 0;
safe_strcat_path(filter, "*", sizeof(filter));
path_append_extension(filter, extension, sizeof(filter));
fileEnumHandle = platform_enumerate_files_begin(filter);
while (platform_enumerate_files_next(fileEnumHandle, &fileInfo)) {
safe_strcpy(newPath, newDirectory, sizeof(newPath));
safe_strcat_path(newPath, fileInfo.path, sizeof(newPath));
safe_strcpy(oldPath, originalDirectory, sizeof(oldPath));
ch = strchr(oldPath, '*');
if (ch != NULL)
*ch = 0;
safe_strcat_path(oldPath, fileInfo.path, sizeof(oldPath));
if (!platform_file_exists(newPath))
platform_file_copy(oldPath, newPath, false);
}
platform_enumerate_files_end(fileEnumHandle);
fileEnumHandle = platform_enumerate_directories_begin(originalDirectory);
while (platform_enumerate_directories_next(fileEnumHandle, filter)) {
safe_strcpy(newPath, newDirectory, sizeof(newPath));
safe_strcat_path(newPath, filter, sizeof(newPath));
safe_strcpy(oldPath, originalDirectory, MAX_PATH);
ch = strchr(oldPath, '*');
if (ch != NULL)
*ch = 0;
safe_strcat_path(oldPath, filter, sizeof(oldPath));
if (!platform_ensure_directory_exists(newPath)) {
log_error("Could not create directory %s.", newPath);
return;
}
openrct2_copy_files_over(oldPath, newPath, extension);
}
platform_enumerate_directories_end(fileEnumHandle);
}
static void openrct2_set_exe_path()
{
platform_get_exe_path(gExePath, sizeof(gExePath));
log_verbose("Setting exe path to %s", gExePath);
}
/**
* Copy saved games and landscapes to user directory
*/
static void openrct2_copy_original_user_files_over()
{
utf8 path[MAX_PATH];
platform_get_user_directory(path, "save", sizeof(path));
openrct2_copy_files_over((utf8*)gRCT2AddressSavedGamesPath, path, ".sv6");
platform_get_user_directory(path, "landscape", sizeof(path));
openrct2_copy_files_over((utf8*)gRCT2AddressLandscapesPath, path, ".sc6");
}
bool openrct2_initialise()
{
utf8 userPath[MAX_PATH];
#ifndef DISABLE_NETWORK
gHashCTX = EVP_MD_CTX_create();
assert(gHashCTX != NULL);
#endif // DISABLE_NETWORK
platform_resolve_openrct_data_path();
platform_resolve_user_data_path();
platform_get_user_directory(userPath, NULL, sizeof(userPath));
if (!platform_ensure_directory_exists(userPath)) {
log_fatal("Could not create user directory (do you have write access to your documents folder?)");
return false;
}
crash_init();
if (!openrct2_setup_rct2_segment()) {
log_fatal("Unable to load RCT2 data sector");
return false;
}
openrct2_set_exe_path();
config_set_defaults();
if (!config_open_default()) {
if (!config_find_or_browse_install_directory()) {
gConfigGeneral.last_run_version = strndup(OPENRCT2_VERSION, strlen(OPENRCT2_VERSION));
config_save_default();
utf8 path[MAX_PATH];
config_get_default_path(path, sizeof(path));
log_fatal("An RCT2 install directory must be specified! Please edit \"game_path\" in %s.", path);
return false;
}
}
gOpenRCT2ShowChangelog = true;
if (gConfigGeneral.last_run_version != NULL && (strcmp(gConfigGeneral.last_run_version, OPENRCT2_VERSION) == 0))
gOpenRCT2ShowChangelog = false;
gConfigGeneral.last_run_version = strndup(OPENRCT2_VERSION, strlen(OPENRCT2_VERSION));
config_save_default();
// TODO add configuration option to allow multiple instances
// if (!gOpenRCT2Headless && !platform_lock_single_instance()) {
// log_fatal("OpenRCT2 is already running.");
// return false;
// }
if (!rct2_init_directories()) {
return false;
}
if (!rct2_startup_checks()) {
return false;
}
if (!gOpenRCT2Headless) {
audio_init();
audio_populate_devices();
}
if (!language_open(gConfigGeneral.language)) {
log_error("Failed to open configured language...");
if (!language_open(LANGUAGE_ENGLISH_UK)) {
log_fatal("Failed to open fallback language...");
return false;
}
}
http_init();
theme_manager_initialise();
title_sequences_set_default();
title_sequences_load_presets();
openrct2_setup_rct2_hooks();
if (!rct2_init())
return false;
chat_init();
openrct2_copy_original_user_files_over();
return true;
}
/**
* Launches the game, after command line arguments have been parsed and processed.
*/
void openrct2_launch()
{
if (openrct2_initialise()) {
gIntroState = INTRO_STATE_NONE;
if((gOpenRCT2StartupAction == STARTUP_ACTION_TITLE) && gConfigGeneral.play_intro)
gOpenRCT2StartupAction = STARTUP_ACTION_INTRO;
switch (gOpenRCT2StartupAction) {
case STARTUP_ACTION_INTRO:
gIntroState = INTRO_STATE_PUBLISHER_BEGIN;
title_load();
break;
case STARTUP_ACTION_TITLE:
title_load();
break;
case STARTUP_ACTION_OPEN:
assert(gOpenRCT2StartupActionPath != NULL);
#ifndef DISABLE_HTTP
// A path that includes "://" is illegal with all common filesystems, so it is almost certainly a URL
// This way all cURL supported protocols, like http, ftp, scp and smb are automatically handled
if (strstr(gOpenRCT2StartupActionPath, "://") != NULL) {
// Download park and open it using its temporary filename
char tmpPath[L_tmpnam];
if (!http_download_park(gOpenRCT2StartupActionPath, tmpPath)) {
title_load();
break;
}
strcpy(gOpenRCT2StartupActionPath, tmpPath);
}
#endif
if (!rct2_open_file(gOpenRCT2StartupActionPath)) {
fprintf(stderr, "Failed to load '%s'", gOpenRCT2StartupActionPath);
title_load();
break;
}
gScreenFlags = SCREEN_FLAGS_PLAYING;
#ifndef DISABLE_NETWORK
if (gNetworkStart == NETWORK_MODE_SERVER) {
if (gNetworkStartPort == 0) {
gNetworkStartPort = gConfigNetwork.default_port;
}
if (str_is_null_or_empty(gCustomPassword)) {
network_set_password(gConfigNetwork.default_password);
}
else {
network_set_password(gCustomPassword);
}
network_begin_server(gNetworkStartPort);
}
#endif // DISABLE_NETWORK
break;
case STARTUP_ACTION_EDIT:
if (strlen(gOpenRCT2StartupActionPath) == 0) {
editor_load();
} else {
if (!editor_load_landscape(gOpenRCT2StartupActionPath)) {
title_load();
}
}
break;
}
#ifndef DISABLE_NETWORK
if (gNetworkStart == NETWORK_MODE_CLIENT) {
if (gNetworkStartPort == 0) {
gNetworkStartPort = gConfigNetwork.default_port;
}
network_begin_client(gNetworkStartHost, gNetworkStartPort);
}
#endif // DISABLE_NETWORK
openrct2_loop();
}
openrct2_dispose();
// HACK Some threads are still running which causes the game to not terminate. Investigation required!
exit(gExitCode);
}
void openrct2_dispose()
{
network_close();
http_dispose();
language_close_all();
rct2_dispose();
config_release();
#ifndef DISABLE_NETWORK
EVP_MD_CTX_destroy(gHashCTX);
#endif // DISABLE_NETWORK
#if defined(USE_MMAP) && (defined(__unix__) || defined(__MACOSX__)) && !defined(NO_RCT2)
munmap(segments, 12079104);
close(fdData);
#endif
platform_free();
}
/**
* Determines whether its worth tweening a sprite or not when frame smoothing is on.
*/
static bool sprite_should_tween(rct_sprite *sprite)
{
switch (sprite->unknown.linked_list_type_offset >> 1) {
case SPRITE_LIST_VEHICLE:
case SPRITE_LIST_PEEP:
case SPRITE_LIST_UNKNOWN:
return true;
}
return false;
}
/**
* Run the main game loop until the finished flag is set at 40fps (25ms interval).
*/
static void openrct2_loop()
{
uint32 currentTick, ticksElapsed, lastTick = 0;
static uint32 uncapTick = 0;
static int fps = 0;
static uint32 secondTick = 0;
log_verbose("begin openrct2 loop");
_finished = 0;
do {
bool is_minimised = (SDL_GetWindowFlags(gWindow) & (SDL_WINDOW_MINIMIZED | SDL_WINDOW_HIDDEN)) != 0;
if (gConfigGeneral.uncap_fps && gGameSpeed <= 4 && !gOpenRCT2Headless && !is_minimised) {
currentTick = SDL_GetTicks();
if (uncapTick == 0) {
// Reset sprite locations
uncapTick = SDL_GetTicks();
openrct2_reset_object_tween_locations();
}
// Limit number of updates per loop (any long pauses or debugging can make this update for a very long time)
if (currentTick - uncapTick > 25 * 60) {
uncapTick = currentTick - 25 - 1;
}
platform_process_messages();
while (uncapTick <= currentTick && currentTick - uncapTick > 25) {
// Get the original position of each sprite
store_sprite_locations(_spritelocations1);
// Update the game so the sprite positions update
rct2_update();
// Get the next position of each sprite
store_sprite_locations(_spritelocations2);
uncapTick += 25;
}
// Tween the position of each sprite from the last position to the new position based on the time between the last
// tick and the next tick.
float nudge = 1 - ((float)(currentTick - uncapTick) / 25);
for (uint16 i = 0; i < MAX_SPRITES; i++) {
if (!sprite_should_tween(get_sprite(i)))
continue;
sprite_set_coordinates(
_spritelocations2[i].x + (sint16)((_spritelocations1[i].x - _spritelocations2[i].x) * nudge),
_spritelocations2[i].y + (sint16)((_spritelocations1[i].y - _spritelocations2[i].y) * nudge),
_spritelocations2[i].z + (sint16)((_spritelocations1[i].z - _spritelocations2[i].z) * nudge),
get_sprite(i)
);
invalidate_sprite_2(get_sprite(i));
}
platform_draw();
fps++;
if (SDL_GetTicks() - secondTick >= 1000) {
fps = 0;
secondTick = SDL_GetTicks();
}
// Restore the real positions of the sprites so they aren't left at the mid-tween positions
for (uint16 i = 0; i < MAX_SPRITES; i++) {
if (!sprite_should_tween(get_sprite(i)))
continue;
invalidate_sprite_2(get_sprite(i));
sprite_set_coordinates(_spritelocations2[i].x, _spritelocations2[i].y, _spritelocations2[i].z, get_sprite(i));
}
} else {
uncapTick = 0;
currentTick = SDL_GetTicks();
ticksElapsed = currentTick - lastTick;
if (ticksElapsed < 25) {
SDL_Delay(25 - ticksElapsed);
lastTick += 25;
} else {
lastTick = currentTick;
}
platform_process_messages();
rct2_update();
if (!is_minimised) {
platform_draw();
}
}
} while (!_finished);
}
/**
* Causes the OpenRCT2 game loop to finish.
*/
void openrct2_finish()
{
_finished = 1;
}
void openrct2_reset_object_tween_locations()
{
for (uint16 i = 0; i < MAX_SPRITES; i++) {
_spritelocations1[i].x = _spritelocations2[i].x = get_sprite(i)->unknown.x;
_spritelocations1[i].y = _spritelocations2[i].y = get_sprite(i)->unknown.y;
_spritelocations1[i].z = _spritelocations2[i].z = get_sprite(i)->unknown.z;
}
}
static void openrct2_get_segment_data_path(char * buffer, size_t bufferSize)
{
platform_get_exe_path(buffer, bufferSize);
safe_strcat_path(buffer, "openrct2_data", bufferSize);
}
/**
* Loads RCT2's data model and remaps the addresses.
* @returns true if the data integrity check succeeded, otherwise false.
*/
bool openrct2_setup_rct2_segment()
{
// OpenRCT2 on Linux and macOS is wired to have the original Windows PE sections loaded
// necessary. Windows does not need to do this as OpenRCT2 runs as a DLL loaded from the Windows PE.
int len = 0x01429000 - 0x8a4000; // 0xB85000, 12079104 bytes or around 11.5MB
int err = 0;
// in some configurations err and len may be unused
UNUSED(err);
UNUSED(len);
#if defined(USE_MMAP) && (defined(__unix__) || defined(__MACOSX__)) && !defined(NO_RCT2)
#define RDATA_OFFSET 0x004A4000
#define DATASEG_OFFSET 0x005E2000
// Using PE-bear I was able to figure out all the needed addresses to be filled.
// There are three sections to be loaded: .rdata, .data and .text, plus another
// one to be mapped: DATASEG.
// Out of the three, two can simply be mmapped into memory, while the third one,
// .data has a virtual size which is much completely different to its file size
// (even when taking page-alignment into consideration)
//
// The sections are as follows (dump from gdb)
// [0] 0x401000->0x6f7000 at 0x00001000: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
// [1] 0x6f7000->0x8a325d at 0x002f7000: CODESEG ALLOC LOAD READONLY CODE HAS_CONTENTS
// [2] 0x8a4000->0x9a5894 at 0x004a4000: .rdata ALLOC LOAD DATA HAS_CONTENTS
// [3] 0x9a6000->0x9e2000 at 0x005a6000: .data ALLOC LOAD DATA HAS_CONTENTS
// [4] 0x1428000->0x14282bc at 0x005e2000: DATASEG ALLOC LOAD DATA HAS_CONTENTS
// [5] 0x1429000->0x1452000 at 0x005e3000: .cms_t ALLOC LOAD READONLY CODE HAS_CONTENTS
// [6] 0x1452000->0x14aaf3e at 0x0060c000: .cms_d ALLOC LOAD DATA HAS_CONTENTS
// [7] 0x14ab000->0x14ac58a at 0x00665000: .idata ALLOC LOAD READONLY DATA HAS_CONTENTS
// [8] 0x14ad000->0x14b512f at 0x00667000: .rsrc ALLOC LOAD DATA HAS_CONTENTS
//
// .data section, however, has virtual size of 0xA81C3C, and so
// 0x9a6000 + 0xA81C3C = 0x1427C3C, which after alignment to page size becomes
// 0x1428000, which can be seen as next section, DATASEG
//
// The data is now loaded into memory with a linker script, which proves to
// be more reliable, as mallocs that happen before we reach segment setup
// could have already taken the space we need.
// TODO: UGLY, UGLY HACK!
//off_t file_size = 6750208;
utf8 segmentDataPath[MAX_PATH];
openrct2_get_segment_data_path(segmentDataPath, sizeof(segmentDataPath));
fdData = open(segmentDataPath, O_RDONLY);
if (fdData < 0)
{
log_fatal("failed to load openrct2_data");
exit(1);
}
log_warning("%p", GOOD_PLACE_FOR_DATA_SEGMENT);
segments = mmap((void *)(GOOD_PLACE_FOR_DATA_SEGMENT), len, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE, fdData, 0);
log_warning("%p", segments);
if ((uintptr_t)segments != GOOD_PLACE_FOR_DATA_SEGMENT) {
perror("mmap");
return false;
}
#endif // defined(USE_MMAP) && (defined(__unix__) || defined(__MACOSX__))
#if defined(__unix__) && !defined(NO_RCT2)
int pageSize = getpagesize();
int numPages = (len + pageSize - 1) / pageSize;
unsigned char *dummy = malloc(numPages);
err = mincore((void *)segments, len, dummy);
bool pagesMissing = false;
if (err != 0)
{
err = errno;
#ifdef __LINUX__
// On Linux ENOMEM means all requested range is unmapped
if (err != ENOMEM)
{
pagesMissing = true;
perror("mincore");
}
#else
pagesMissing = true;
perror("mincore");
#endif // __LINUX__
} else {
for (int i = 0; i < numPages; i++)
{
if (dummy[i] != 1)
{
pagesMissing = true;
void *start = (void *)segments + i * pageSize;
void *end = (void *)segments + (i + 1) * pageSize - 1;
log_warning("required page %p - %p is not in memory!", start, end);
}
}
}
free(dummy);
if (pagesMissing)
{
log_error("At least one of required pages was not found in memory. This can cause segfaults later on.");
}
#if !defined(USE_MMAP)
// section: text
err = mprotect((void *)0x401000, 0x8a4000 - 0x401000, PROT_READ | PROT_EXEC | PROT_WRITE);
if (err != 0)
{
perror("mprotect");
}
#endif // !defined(USE_MMAP)
// section: rw data
err = mprotect((void *)segments, 0x01429000 - 0x8a4000, PROT_READ | PROT_WRITE);
if (err != 0)
{
perror("mprotect");
}
#endif // defined(__unix__)
#if defined(USE_MMAP) && defined(__WINDOWS__)
segments = VirtualAlloc((void *)(GOOD_PLACE_FOR_DATA_SEGMENT), len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if ((uintptr_t)segments != GOOD_PLACE_FOR_DATA_SEGMENT) {
log_error("VirtualAlloc, segments = %p, GetLastError = 0x%x", segments, GetLastError());
return false;
}
utf8 segmentDataPath[MAX_PATH];
openrct2_get_segment_data_path(segmentDataPath, sizeof(segmentDataPath));
SDL_RWops * rw = SDL_RWFromFile(segmentDataPath, "rb");
if (rw == NULL)
{
log_error("failed to load file");
return false;
}
if (SDL_RWread(rw, segments, len, 1) != 1) {
log_error("Unable to read chunk header!");
return false;
}
SDL_RWclose(rw);
#endif // defined(USE_MMAP) && defined(__WINDOWS__)
#if !defined(NO_RCT2) && defined(USE_MMAP)
// Check that the expected data is at various addresses.
// Start at 0x9a6000, which is start of .data, to skip the region containing addresses to DLL
// calls, which can be changed by windows/wine loader.
const uint32 c1 = sawyercoding_calculate_checksum((const uint8*)(segments + (uintptr_t)(0x009A6000 - 0x8a4000)), 0x009E0000 - 0x009A6000);
const uint32 c2 = sawyercoding_calculate_checksum((const uint8*)(segments + (uintptr_t)(0x01428000 - 0x8a4000)), 0x014282BC - 0x01428000);
const uint32 exp_c1 = 10114815;
const uint32 exp_c2 = 23564;
if (c1 != exp_c1 || c2 != exp_c2) {
log_warning("c1 = %u, expected %u, match %d", c1, exp_c1, c1 == exp_c1);
log_warning("c2 = %u, expected %u, match %d", c2, exp_c2, c2 == exp_c2);
return false;
}
#endif
return true;
}
/**
* Setup hooks to allow RCT2 to call OpenRCT2 functions instead.
*/
static void openrct2_setup_rct2_hooks()
{
// None for now
}
#if defined(_MSC_VER) && (_MSC_VER >= 1900)
/**
* Temporary fix for libraries not compiled with VS2015
*/
FILE **__iob_func()
{
static FILE* streams[3];
streams[0] = stdin;
streams[1] = stdout;
streams[2] = stderr;
return streams;
}
#endif

View File

@ -141,7 +141,7 @@ static void sort_servers();
static void join_server(char *address);
static void fetch_servers();
#ifndef DISABLE_HTTP
static void fetch_servers_callback(http_json_response* response);
static void fetch_servers_callback(http_response_t* response);
#endif
void window_server_list_open()
@ -775,16 +775,17 @@ static void fetch_servers()
sort_servers();
SDL_UnlockMutex(_mutex);
http_json_request request;
http_request_t request;
request.url = masterServerUrl;
request.method = HTTP_METHOD_GET;
request.body = NULL;
http_request_json_async(&request, fetch_servers_callback);
request.type = HTTP_DATA_JSON;
http_request_async(&request, fetch_servers_callback);
#endif
}
#ifndef DISABLE_HTTP
static void fetch_servers_callback(http_json_response* response)
static void fetch_servers_callback(http_response_t* response)
{
if (response == NULL) {
log_warning("Unable to connect to master server");
@ -793,21 +794,21 @@ static void fetch_servers_callback(http_json_response* response)
json_t *jsonStatus = json_object_get(response->root, "status");
if (!json_is_number(jsonStatus)) {
http_request_json_dispose(response);
http_request_dispose(response);
log_warning("Invalid response from master server");
return;
}
int status = (int)json_integer_value(jsonStatus);
if (status != 200) {
http_request_json_dispose(response);
http_request_dispose(response);
log_warning("Master server failed to return servers");
return;
}
json_t *jsonServers = json_object_get(response->root, "servers");
if (!json_is_array(jsonServers)) {
http_request_json_dispose(response);
http_request_dispose(response);
log_warning("Invalid response from master server");
return;
}
@ -852,7 +853,7 @@ static void fetch_servers_callback(http_json_response* response)
newserver->maxplayers = (uint8)json_integer_value(maxPlayers);
SDL_UnlockMutex(_mutex);
}
http_request_json_dispose(response);
http_request_dispose(response);
sort_servers();
_numPlayersOnline = get_total_player_count();