diff --git a/src/config.c b/src/config.c index ab3a29f8c4..d111a03ecf 100644 --- a/src/config.c +++ b/src/config.c @@ -197,11 +197,21 @@ config_property_definition _cheatDefinitions[] = { { offsetof(cheat_configuration, unlock_all_prices), "unlock_all_prices", CONFIG_VALUE_TYPE_BOOLEAN, false, NULL }, }; +config_property_definition _twitchDefinitions[] = { + { offsetof(twitch_configuration, channel), "channel", CONFIG_VALUE_TYPE_STRING, { .value_string = NULL }, NULL }, + { offsetof(twitch_configuration, enable_follower_peep_names), "follower_peep_names", CONFIG_VALUE_TYPE_BOOLEAN, true, NULL }, + { offsetof(twitch_configuration, enable_follower_peep_tracking), "follower_peep_tracking", CONFIG_VALUE_TYPE_BOOLEAN, false, NULL }, + { offsetof(twitch_configuration, enable_chat_peep_names), "chat_peep_names", CONFIG_VALUE_TYPE_BOOLEAN, true, NULL }, + { offsetof(twitch_configuration, enable_chat_peep_tracking), "chat_peep_tracking", CONFIG_VALUE_TYPE_BOOLEAN, true, NULL }, + { offsetof(twitch_configuration, enable_news), "news", CONFIG_VALUE_TYPE_BOOLEAN, false, NULL } +}; + config_section_definition _sectionDefinitions[] = { { &gConfigGeneral, "general", _generalDefinitions, countof(_generalDefinitions) }, { &gConfigInterface, "interface", _interfaceDefinitions, countof(_interfaceDefinitions) }, { &gConfigSound, "sound", _soundDefinitions, countof(_soundDefinitions) }, - { &gConfigCheat, "cheat", _cheatDefinitions, countof(_cheatDefinitions) } + { &gConfigCheat, "cheat", _cheatDefinitions, countof(_cheatDefinitions) }, + { &gConfigTwitch, "twitch", _twitchDefinitions, countof(_twitchDefinitions) } }; #pragma endregion @@ -210,6 +220,7 @@ general_configuration gConfigGeneral; interface_configuration gConfigInterface; sound_configuration gConfigSound; cheat_configuration gConfigCheat; +twitch_configuration gConfigTwitch; bool config_open(const utf8string path); bool config_save(const utf8string path); diff --git a/src/config.h b/src/config.h index 104e6a9b34..bdd3c363e9 100644 --- a/src/config.h +++ b/src/config.h @@ -165,6 +165,14 @@ typedef struct { uint8 unlock_all_prices; } cheat_configuration; +typedef struct { + utf8string channel; + uint8 enable_follower_peep_names; + uint8 enable_follower_peep_tracking; + uint8 enable_chat_peep_names; + uint8 enable_chat_peep_tracking; + uint8 enable_news; +} twitch_configuration; typedef struct { uint8 key; @@ -175,6 +183,7 @@ extern general_configuration gConfigGeneral; extern interface_configuration gConfigInterface; extern sound_configuration gConfigSound; extern cheat_configuration gConfigCheat; +extern twitch_configuration gConfigTwitch; extern uint16 gShortcutKeys[SHORTCUT_COUNT]; diff --git a/src/interface/console.c b/src/interface/console.c index 86b55eb7ed..4dafd92d12 100644 --- a/src/interface/console.c +++ b/src/interface/console.c @@ -1,5 +1,6 @@ #include #include + #include "../addresses.h" #include "../drawing/drawing.h" #include "../localisation/localisation.h" @@ -10,11 +11,12 @@ #include "../cursors.h" #include "../game.h" #include "../input.h" +#include "../network/twitch.h" #include "../object.h" -#include "console.h" -#include "window.h" #include "../world/scenery.h" #include "../management/research.h" +#include "console.h" +#include "window.h" #define CONSOLE_BUFFER_SIZE 8192 #define CONSOLE_BUFFER_2_SIZE 256 @@ -635,6 +637,15 @@ static int cc_set(const char **argv, int argc) } return 0; } +static int cc_twitch(const char **argv, int argc) +{ +#ifdef DISABLE_TWITCH + console_writeline_error("OpenRCT2 build not compiled with Twitch integeration."); +#else + // TODO add some twitch commands +#endif + return 0; +} static void editor_load_selected_objects_console() { uint8 *selection_flags = RCT2_GLOBAL(RCT2_ADDRESS_EDITOR_OBJECT_FLAGS_LIST, uint8*); @@ -694,7 +705,6 @@ static int cc_load_object(const char **argv, int argc) { reset_loaded_objects(); if (type == OBJECT_TYPE_RIDE) { // Automatically research the ride so it's supported by the game. - rct_ride_type *rideEntry; int rideType; @@ -833,7 +843,8 @@ console_command console_command_table[] = { "Loading a scenery group will not load its associated objects.\n" "This is a safer method opposed to \"open object_selection\".", "load_object " }, - { "object_count", cc_object_count, "Shows the number of objects of each type in the scenario.", "object_count" } + { "object_count", cc_object_count, "Shows the number of objects of each type in the scenario.", "object_count" }, + { "twitch", cc_twitch, "Twitch API" } }; static int cc_windows(const char **argv, int argc) { diff --git a/src/interface/console.h b/src/interface/console.h index 26ae199c42..3c11077edb 100644 --- a/src/interface/console.h +++ b/src/interface/console.h @@ -2,6 +2,7 @@ #define _CONSOLE_H_ #include "../common.h" +#include "../drawing/drawing.h" extern bool gConsoleOpen; diff --git a/src/management/news_item.c b/src/management/news_item.c index e1be0d3572..188f17ba6e 100644 --- a/src/management/news_item.c +++ b/src/management/news_item.c @@ -249,6 +249,15 @@ void news_item_get_subject_location(int type, int subject, int *x, int *y, int * * @param c (ecx) **/ void news_item_add_to_queue(uint8 type, rct_string_id string_id, uint32 assoc) +{ + char *buffer = (char*)0x0141EF68; + void *args = (void*)0x013CE952; + + format_string(buffer, string_id, args); // overflows possible? + news_item_add_to_queue_raw(type, buffer, assoc); +} + +void news_item_add_to_queue_raw(uint8 type, const char *text, uint32 assoc) { int i = 0; rct_news_item *newsItem = RCT2_ADDRESS(RCT2_ADDRESS_NEWS_ITEM_LIST, rct_news_item); @@ -268,10 +277,8 @@ void news_item_add_to_queue(uint8 type, rct_string_id string_id, uint32 assoc) newsItem->ticks = 0; newsItem->month_year = RCT2_GLOBAL(RCT2_ADDRESS_CURRENT_MONTH_YEAR, uint16); newsItem->day = ((days_in_month[(newsItem->month_year & 7)] * RCT2_GLOBAL(RCT2_ADDRESS_CURRENT_MONTH_TICKS, uint16)) >> 16) + 1; - - format_string((char*)0x0141EF68, string_id, (void*)0x013CE952); // overflows possible? - newsItem->colour = ((char*)0x0141EF68)[0]; - strncpy(newsItem->text, (char*)0x0141EF68, 255); + newsItem->colour = text[0]; + strncpy(newsItem->text, text + 1, 254); newsItem->text[254] = 0; // blatant disregard for what happens on the last element. diff --git a/src/management/news_item.h b/src/management/news_item.h index 83ddd57d90..34d4459147 100644 --- a/src/management/news_item.h +++ b/src/management/news_item.h @@ -57,6 +57,7 @@ void news_item_update_current(); void news_item_close_current(); void news_item_get_subject_location(int type, int subject, int *x, int *y, int *z); void news_item_add_to_queue(uint8 type, rct_string_id string_id, uint32 assoc); +void news_item_add_to_queue_raw(uint8 type, const char *text, uint32 assoc); void news_item_open_subject(int type, int subject); void news_item_disable_news(uint8 type, uint32 assoc); diff --git a/src/network/http.cpp b/src/network/http.cpp new file mode 100644 index 0000000000..790ce89b3f --- /dev/null +++ b/src/network/http.cpp @@ -0,0 +1,147 @@ +extern "C" { + #include "http.h" +} + +#ifdef DISABLE_HTTP + +void http_init() { } +void http_dispose() { } + +#else + +#include +#include +#include + +typedef struct { + char *ptr; + int length; + int capacity; +} write_buffer; + +void http_init() +{ + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +void http_dispose() +{ + curl_global_cleanup(); +} + +static size_t http_request_write_func(void *ptr, size_t size, size_t nmemb, void *userdata) +{ + write_buffer *writeBuffer = (write_buffer*)userdata; + + int newBytesLength = size * nmemb; + if (newBytesLength > 0) { + int newCapacity = writeBuffer->capacity; + int newLength = writeBuffer->length + newBytesLength; + while (newLength > newCapacity) { + newCapacity = max(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_json_response *http_request_json(const char *url) +{ + CURL *curl; + CURLcode curlResult; + http_json_response *response; + write_buffer writeBuffer; + + curl = curl_easy_init(); + if (curl == NULL) + return NULL; + + writeBuffer.ptr = NULL; + writeBuffer.length = 0; + writeBuffer.capacity = 0; + + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, TRUE); + curl_easy_setopt(curl, CURLOPT_CAINFO, "curl-ca-bundle.crt"); + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeBuffer); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_request_write_func); + + curlResult = curl_easy_perform(curl); + 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->status_code = (int)httpStatusCode; + response->root = root; + } + free(writeBuffer.ptr); + return response; +} + +void http_request_json_async(const char *url, void (*callback)(http_json_response*)) +{ + struct TempThreadArgs { + const char *url; + void (*callback)(http_json_response*); + }; + + TempThreadArgs *args = (TempThreadArgs*)malloc(sizeof(TempThreadArgs)); + args->url = url; + args->callback = callback; + + SDL_Thread *thread = SDL_CreateThread([](void *ptr) -> int { + TempThreadArgs *args = (TempThreadArgs*)ptr; + + http_json_response *response = http_request_json(args->url); + args->callback(response); + 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); +} + +#endif \ No newline at end of file diff --git a/src/network/http.h b/src/network/http.h new file mode 100644 index 0000000000..ffc051a235 --- /dev/null +++ b/src/network/http.h @@ -0,0 +1,18 @@ +#ifndef _HTTP_H_ +#define _HTTP_H_ + +#include +#include "../common.h" + +typedef struct { + int status_code; + json_t *root; +} http_json_response; + +void http_init(); +void http_dispose(); +http_json_response *http_request_json(const char *url); +void http_request_json_async(const char *url, void (*callback)(http_json_response*)); +void http_request_json_dispose(http_json_response *response); + +#endif diff --git a/src/network/twitch.cpp b/src/network/twitch.cpp new file mode 100644 index 0000000000..1e36c0526b --- /dev/null +++ b/src/network/twitch.cpp @@ -0,0 +1,435 @@ +#ifdef DISABLE_TWITCH + + extern "C" { + #include "twitch.h" + } + + void twitch_update() { } + +#else + +// REQUIRES HTTP + +#include +#include + +extern "C" { + + #include "../addresses.h" + #include "../config.h" + #include "../interface/console.h" + #include "../localisation/localisation.h" + #include "../management/news_item.h" + #include "../peep/peep.h" + #include "../world/sprite.h" + #include "http.h" + #include "twitch.h" + +} + +enum { + TWITCH_STATE_JOINING, + TWITCH_STATE_JOINED, + TWITCH_STATE_WAITING, + TWITCH_STATE_GET_FOLLOWERS, + TWITCH_STATE_GET_MESSAGES, + TWITCH_STATE_LEAVING, + TWITCH_STATE_LEFT +}; + +// The time between HTTP requests. +// TODO Ideally, the chat message pulse should be more frequent than the followers / chat members so that news messages etc. +// have a lower latency. +#define PULSE_TIME (10 * 1000) + +const char *TwitchExtendedBaseUrl = "http://openrct.ursalabs.co/api/1/"; + +bool gTwitchEnable = false; + +static int _twitchState = TWITCH_STATE_LEFT; +static bool _twitchIdle = true; +static uint32 _twitchLastPulseTick = 0; +static int _twitchLastPulseOperation = 1; +static http_json_response *_twitchJsonResponse; + +static void twitch_join(); +static void twitch_leave(); +static void twitch_get_followers(); +static void twitch_get_messages(); + +static void twitch_parse_followers(); +static void twitch_parse_messages(); +static void twitch_parse_chat_message(const char *message); + +void twitch_update() +{ + if (!_twitchIdle) + return; + + bool twitchable = + !(RCT2_GLOBAL(RCT2_ADDRESS_SCREEN_FLAGS, uint8) & (~SCREEN_FLAGS_PLAYING)) && + gConfigTwitch.channel != NULL && + gConfigTwitch.channel[0] != 0 && + gTwitchEnable; + + if (twitchable) { + if (RCT2_GLOBAL(RCT2_ADDRESS_GAME_PAUSED, uint8) != 0) + return; + + switch (_twitchState) { + case TWITCH_STATE_LEFT: + { + uint32 currentTime = SDL_GetTicks(); + uint32 timeSinceLastPulse = currentTime - _twitchLastPulseTick; + if (_twitchLastPulseTick == 0 || timeSinceLastPulse > PULSE_TIME) { + _twitchLastPulseTick = currentTime; + twitch_join(); + } + break; + } + case TWITCH_STATE_JOINED: + { + uint32 currentTime = SDL_GetTicks(); + uint32 timeSinceLastPulse = currentTime - _twitchLastPulseTick; + if (_twitchLastPulseTick == 0 || timeSinceLastPulse > PULSE_TIME) { + _twitchLastPulseTick = currentTime; + _twitchLastPulseOperation = (_twitchLastPulseOperation + 1) % 2; + switch (_twitchLastPulseOperation + TWITCH_STATE_GET_FOLLOWERS) { + case TWITCH_STATE_GET_FOLLOWERS: + twitch_get_followers(); + break; + case TWITCH_STATE_GET_MESSAGES: + if (gConfigTwitch.enable_news) + twitch_get_messages(); + break; + } + } + break; + } + case TWITCH_STATE_GET_FOLLOWERS: + twitch_parse_followers(); + break; + case TWITCH_STATE_GET_MESSAGES: + twitch_parse_messages(); + break; + } + } else { + if (_twitchState != TWITCH_STATE_LEFT) + twitch_leave(); + } +} + +/** + * GET /leave/:join + */ +static void twitch_join() +{ + char url[256]; + sprintf(url, "%sjoin/%s", TwitchExtendedBaseUrl, gConfigTwitch.channel); + + _twitchState = TWITCH_STATE_JOINING; + _twitchIdle = false; + http_request_json_async(url, [](http_json_response *jsonResponse) -> void { + if (jsonResponse == NULL) { + _twitchState = TWITCH_STATE_LEFT; + console_writeline("Unable to connect to twitch channel."); + } else { + json_t *jsonStatus = json_object_get(jsonResponse->root, "status"); + if (json_is_number(jsonStatus) && json_integer_value(jsonStatus) == 200) + _twitchState = TWITCH_STATE_JOINED; + else + _twitchState = TWITCH_STATE_LEFT; + + http_request_json_dispose(jsonResponse); + + _twitchLastPulseTick = 0; + console_writeline("Connected to twitch channel."); + } + _twitchIdle = true; + }); +} + +/** + * GET /leave/:channel + */ +static void twitch_leave() +{ + if (_twitchJsonResponse != NULL) { + http_request_json_dispose(_twitchJsonResponse); + _twitchJsonResponse = NULL; + } + + console_writeline("Left twitch channel."); + _twitchState = TWITCH_STATE_LEFT; + _twitchLastPulseTick = 0; + gTwitchEnable = false; + + // TODO reset all peeps with twitch flag + + // HTTP request no longer used as it could be abused + // char url[256]; + // sprintf(url, "%sleave/%s", TwitchExtendedBaseUrl, gConfigTwitch.channel); + // _twitchState = TWITCH_STATE_LEAVING; + // _twitchIdle = false; + // http_request_json_async(url, [](http_json_response *jsonResponse) -> void { + // http_request_json_dispose(jsonResponse); + // _twitchState = TWITCH_STATE_LEFT; + // _twitchIdle = true; + // + // console_writeline("Left twitch channel."); + // }); +} + +/** + * GET /channel/:channel/audience + */ +static void twitch_get_followers() +{ + char url[256]; + sprintf(url, "%schannel/%s/audience", TwitchExtendedBaseUrl, gConfigTwitch.channel); + + _twitchState = TWITCH_STATE_WAITING; + _twitchIdle = false; + http_request_json_async(url, [](http_json_response *jsonResponse) -> void { + if (jsonResponse == NULL) { + _twitchState = TWITCH_STATE_JOINED; + } else { + _twitchJsonResponse = jsonResponse; + _twitchState = TWITCH_STATE_GET_FOLLOWERS; + } + _twitchIdle = true; + }); +} + +/** + * GET /channel/:channel/messages + */ +static void twitch_get_messages() +{ + char url[256]; + sprintf(url, "%schannel/%s/messages", TwitchExtendedBaseUrl, gConfigTwitch.channel); + + _twitchState = TWITCH_STATE_WAITING; + _twitchIdle = false; + http_request_json_async(url, [](http_json_response *jsonResponse) -> void { + if (jsonResponse == NULL) { + _twitchState = TWITCH_STATE_JOINED; + } else { + _twitchJsonResponse = jsonResponse; + _twitchState = TWITCH_STATE_GET_MESSAGES; + } + _twitchIdle = true; + }); +} + +static void twitch_parse_followers() +{ + struct AudienceMember { + const char *name; + bool isFollower; + bool isInChat; + bool isMod; + bool exists; + bool shouldTrack; + }; + + std::vector members; + + http_json_response *jsonResponse = _twitchJsonResponse; + if (json_is_array(jsonResponse->root)) { + int audienceCount = json_array_size(jsonResponse->root); + for (int i = 0; i < audienceCount; i++) { + json_t *audienceMember = json_array_get(jsonResponse->root, i); + if (!json_is_object(audienceMember)) + continue; + + json_t *name = json_object_get(audienceMember, "name"); + json_t *isFollower = json_object_get(audienceMember, "isFollower"); + json_t *isInChat = json_object_get(audienceMember, "inChat"); + json_t *isMod = json_object_get(audienceMember, "isMod"); + + AudienceMember member; + member.name = json_string_value(name); + member.isFollower = json_boolean_value(isFollower); + member.isInChat = json_boolean_value(isInChat); + member.isMod = json_boolean_value(isMod); + member.exists = false; + member.shouldTrack = false; + + if (member.name == NULL || member.name[0] == 0) + continue; + + if (member.isInChat && gConfigTwitch.enable_chat_peep_tracking) + member.shouldTrack = true; + else if (member.isFollower && gConfigTwitch.enable_follower_peep_tracking) + member.shouldTrack = true; + + if (gConfigTwitch.enable_chat_peep_names && member.isInChat) + members.push_back(member); + else if (gConfigTwitch.enable_follower_peep_names && member.isFollower) + members.push_back(member); + } + + uint16 spriteIndex; + rct_peep *peep; + char buffer[256]; + + // Check what followers are already in the park + FOR_ALL_GUESTS(spriteIndex, peep) { + if (is_user_string_id(peep->name_string_idx)) { + format_string(buffer, peep->name_string_idx, NULL); + + AudienceMember *member = NULL; + for (size_t i = 0; i < members.size(); i++) { + if (_strcmpi(buffer, members[i].name) == 0) { + member = &members[i]; + members[i].exists = true; + break; + } + } + + if (peep->flags & PEEP_FLAGS_TWITCH) { + if (member == NULL) { + // Member no longer peep name worthy + peep->flags &= ~(PEEP_FLAGS_TRACKING | PEEP_FLAGS_TWITCH); + + // TODO set peep name back to number / real name + } else { + if (member->shouldTrack) + peep->flags |= (PEEP_FLAGS_TRACKING); + else if (!member->shouldTrack) + peep->flags &= ~(PEEP_FLAGS_TRACKING); + } + } else if (member != NULL && !(peep->flags & PEEP_FLAGS_LEAVING_PARK)) { + // Peep with same name already exists but not twitch + peep->flags |= PEEP_FLAGS_TWITCH; + if (member->shouldTrack) + peep->flags |= PEEP_FLAGS_TRACKING; + } + } + } + + // Rename non-named peeps to followers that aren't currently in the park. + if (members.size() > 0) { + int memberIndex = -1; + FOR_ALL_GUESTS(spriteIndex, peep) { + int originalMemberIndex = memberIndex; + for (size_t i = memberIndex + 1; i < members.size(); i++) { + if (!members[i].exists) { + memberIndex = i; + break; + } + } + if (originalMemberIndex == memberIndex) + break; + + AudienceMember *member = &members[memberIndex]; + if (!is_user_string_id(peep->name_string_idx) && !(peep->flags & PEEP_FLAGS_LEAVING_PARK)) { + // Rename peep and add flags + rct_string_id newStringId = user_string_allocate(4, member->name); + if (newStringId != 0) { + peep->name_string_idx = newStringId; + peep->flags |= PEEP_FLAGS_TWITCH; + if (member->shouldTrack) + peep->flags |= PEEP_FLAGS_TRACKING; + } + } else { + // Peep still yet to be found for member + memberIndex--; + } + } + } + } + + http_request_json_dispose(_twitchJsonResponse); + _twitchJsonResponse = NULL; + _twitchState = TWITCH_STATE_JOINED; + + gfx_invalidate_screen(); +} + +static void twitch_parse_messages() +{ + http_json_response *jsonResponse = _twitchJsonResponse; + if (json_is_array(jsonResponse->root)) { + int messageCount = json_array_size(jsonResponse->root); + for (int i = 0; i < messageCount; i++) { + json_t *jsonMessage = json_array_get(jsonResponse->root, i); + if (!json_is_object(jsonMessage)) + continue; + + json_t *jsonText = json_object_get(jsonMessage, "message"); + const char *text = json_string_value(jsonText); + + twitch_parse_chat_message(text); + } + } + + http_request_json_dispose(_twitchJsonResponse); + _twitchJsonResponse = NULL; + _twitchState = TWITCH_STATE_JOINED; +} + +/** + * Like strchr but allows searching for one of many characters. + */ +static char *strchrm(const char *str, const char *find) +{ + const char *result = NULL; + do { + const char *fch = find; + while (*fch != 0) { + if (*str == *fch) + return (char*)str; + + fch++; + } + } while (*str++ != 0); + return NULL; +} + +static char *strskipwhitespace(const char *str) +{ + while (*str == ' ' || *str == '\t') + str++; + + return (char*)str; +} + +static void twitch_parse_chat_message(const char *message) +{ + char buffer[256], *ch; + + message = strskipwhitespace(message); + if (message[0] != '!') + return; + + message++; + ch = strchrm(message, " \t"); + strncpy(buffer, message, ch - message); + buffer[ch - message] = 0; + if (_strcmpi(buffer, "news") == 0) { + if (gConfigTwitch.enable_news) { + ch = strskipwhitespace(ch); + + buffer[0] = (char)FORMAT_TOPAZ; + strncpy(buffer + 1, ch, sizeof(buffer) - 2); + buffer[sizeof(buffer) - 2] = 0; + + // Remove unsupport characters + // TODO allow when OpenRCT2 gains unicode support + ch = buffer; + while (ch[0] != 0) { + if ((unsigned char)ch[0] < 32 || (unsigned char)ch[0] > 122) { + ch[0] = ' '; + } + ch++; + } + + // TODO Create a new news item type for twitch which has twitch icon + news_item_add_to_queue_raw(NEWS_ITEM_BLANK, buffer, 0); + } + } +} + +#endif \ No newline at end of file diff --git a/src/network/twitch.h b/src/network/twitch.h new file mode 100644 index 0000000000..247c721281 --- /dev/null +++ b/src/network/twitch.h @@ -0,0 +1,10 @@ +#ifndef _TWITCH_H_ +#define _TWITCH_H_ + +#include "../common.h" + +extern bool gTwitchEnable; + +void twitch_update(); + +#endif \ No newline at end of file diff --git a/src/openrct2.c b/src/openrct2.c index 7bbc3727fe..c4457a2c09 100644 --- a/src/openrct2.c +++ b/src/openrct2.c @@ -25,6 +25,7 @@ #include "config.h" #include "editor.h" #include "localisation/localisation.h" +#include "network/http.h" #include "openrct2.h" #include "platform/platform.h" #include "util/sawyercoding.h" @@ -141,6 +142,7 @@ void openrct2_launch() audio_get_devices(); get_dsound_devices(); language_open(gConfigGeneral.language); + http_init(); if (!rct2_init()) return; @@ -174,6 +176,8 @@ void openrct2_launch() log_verbose("begin openrct2 loop"); openrct2_loop(); + + http_dispose(); platform_free(); // HACK Some threads are still running which causes the game to not terminate. Investigation required! diff --git a/src/peep/peep.h b/src/peep/peep.h index 9dd9d26a89..a9260f3cb0 100644 --- a/src/peep/peep.h +++ b/src/peep/peep.h @@ -269,7 +269,9 @@ enum PEEP_FLAGS { PEEP_FLAGS_JOY = (1 << 23), // Makes the peep jump in joy PEEP_FLAGS_ANGRY = (1 << 24), - PEEP_FLAGS_ICE_CREAM = (1 << 25) // Unconfirmed + PEEP_FLAGS_ICE_CREAM = (1 << 25), // Unconfirmed + + PEEP_FLAGS_TWITCH = (1 << 31) // Added for twitch integration }; enum PEEP_NAUSEA_TOLERANCE { diff --git a/src/rct2.c b/src/rct2.c index 782aa70038..81dc31e8b5 100644 --- a/src/rct2.c +++ b/src/rct2.c @@ -34,6 +34,7 @@ #include "localisation/date.h" #include "localisation/localisation.h" #include "management/news_item.h" +#include "network/twitch.h" #include "object.h" #include "openrct2.h" #include "platform/platform.h" @@ -346,6 +347,7 @@ void rct2_update_2() else game_update(); + twitch_update(); console_update(); console_draw(RCT2_ADDRESS(RCT2_ADDRESS_SCREEN_DPI, rct_drawpixelinfo)); } diff --git a/src/windows/top_toolbar.c b/src/windows/top_toolbar.c index 9817e3076f..e7c49d44d7 100644 --- a/src/windows/top_toolbar.c +++ b/src/windows/top_toolbar.c @@ -29,6 +29,7 @@ #include "../interface/window.h" #include "../interface/viewport.h" #include "../localisation/localisation.h" +#include "../network/twitch.h" #include "../scenario.h" #include "../world/scenery.h" #include "../world/banner.h" @@ -65,11 +66,15 @@ enum { typedef enum { DDIDX_LOAD_GAME = 0, DDIDX_SAVE_GAME = 1, + // seperator DDIDX_ABOUT = 3, DDIDX_OPTIONS = 4, DDIDX_SCREENSHOT = 5, + // seperator DDIDX_QUIT_TO_MENU = 7, DDIDX_EXIT_OPENRCT2 = 8, + // seperator + DDIDX_ENABLE_TWITCH = 10 } FILE_MENU_DDIDX; typedef enum { @@ -204,6 +209,8 @@ void toggle_land_window(rct_window *topToolbar, int widgetIndex); void toggle_clear_scenery_window(rct_window *topToolbar, int widgetIndex); void toggle_water_window(rct_window *topToolbar, int widgetIndex); +static bool _menuDropdownIncludesTwitch; + /** * Creates the main game top toolbar window. * rct2: 0x0066B485 (part of 0x0066B3E8) @@ -306,6 +313,7 @@ static void window_top_toolbar_mousedown(int widgetIndex, rct_window*w, rct_widg switch (widgetIndex) { case WIDX_FILE_MENU: + _menuDropdownIncludesTwitch = false; if (RCT2_GLOBAL(RCT2_ADDRESS_SCREEN_FLAGS, uint8) & (SCREEN_FLAGS_TRACK_DESIGNER | SCREEN_FLAGS_TRACK_MANAGER)) { gDropdownItemsFormat[0] = STR_ABOUT; gDropdownItemsFormat[1] = STR_OPTIONS; @@ -340,6 +348,16 @@ static void window_top_toolbar_mousedown(int widgetIndex, rct_window*w, rct_widg gDropdownItemsFormat[7] = STR_QUIT_TO_MENU; gDropdownItemsFormat[8] = STR_EXIT_OPENRCT2; numItems = 9; + + #ifndef DISABLE_TWITCH + if (gConfigTwitch.channel != NULL && gConfigTwitch.channel[0] != 0) { + _menuDropdownIncludesTwitch = true; + gDropdownItemsFormat[9] = 0; + gDropdownItemsFormat[10] = 1156; + gDropdownItemsArgs[10] = STR_TWITCH_ENABLE; + numItems = 11; + } + #endif } window_dropdown_show_text( w->x + widget->left, @@ -349,6 +367,9 @@ static void window_top_toolbar_mousedown(int widgetIndex, rct_window*w, rct_widg DROPDOWN_FLAG_STAY_OPEN, numItems ); + + if (_menuDropdownIncludesTwitch && gTwitchEnable) + gDropdownItemsChecked |= (1 << 10); break; case WIDX_VIEW_MENU: top_toolbar_init_view_menu(w, widget); @@ -425,6 +446,9 @@ static void window_top_toolbar_dropdown() case DDIDX_EXIT_OPENRCT2: rct2_quit(); break; + case DDIDX_ENABLE_TWITCH: + gTwitchEnable = !gTwitchEnable; + break; } break; case WIDX_VIEW_MENU: