From f10d79c63cfb51112e4c55b936f6da446fdbd74b Mon Sep 17 00:00:00 2001 From: IntelOrca Date: Sun, 10 Apr 2016 17:02:03 +0100 Subject: [PATCH] refactor and clean up twitch.cpp --- src/network/twitch.cpp | 986 +++++++++++++++++++++++------------------ 1 file changed, 557 insertions(+), 429 deletions(-) diff --git a/src/network/twitch.cpp b/src/network/twitch.cpp index 738df2791d..b52fecd95a 100644 --- a/src/network/twitch.cpp +++ b/src/network/twitch.cpp @@ -1,449 +1,577 @@ #ifdef DISABLE_TWITCH - extern "C" { - #include "twitch.h" - } + extern "C" + { + #include "twitch.h" + } - void twitch_update() { } + void twitch_update() { } #else -// REQUIRES HTTP +#ifdef DISABLE_HTTP + #error HTTP must be enabled to use the TWITCH functionality. +#endif -#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 "../util/util.h" - #include "http.h" - #include "twitch.h" +#include "../core/List.hpp" +#include "../core/Math.hpp" +#include "../core/String.hpp" +extern "C" +{ + #include "../addresses.h" + #include "../config.h" + #include "../drawing/drawing.h" + #include "../interface/console.h" + #include "../localisation/localisation.h" + #include "../management/news_item.h" + #include "../peep/peep.h" + #include "../world/sprite.h" + #include "../util/util.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; +namespace Twitch +{ + enum + { + TWITCH_STATE_JOINING, + TWITCH_STATE_JOINED, + TWITCH_STATE_WAITING, + TWITCH_STATE_GET_FOLLOWERS, + TWITCH_STATE_GET_MESSAGES, + TWITCH_STATE_LEAVING, + TWITCH_STATE_LEFT + }; -static void twitch_join(); -static void twitch_leave(); -static void twitch_get_followers(); -static void twitch_get_messages(); + enum + { + TWITCH_STATUS_OK = 200 + }; -static void twitch_parse_followers(); -static void twitch_parse_messages(); -static void twitch_parse_chat_message(const char *message); + struct AudienceMember + { + const char * Name; + bool IsFollower; + bool IsInChat; + bool IsMod; + bool Exists; + bool ShouldTrack; + + static AudienceMember FromJson(json_t * json) + { + AudienceMember member = { 0 }; + + if (!json_is_object(json)) return member; + json_t * name = json_object_get(json, "name"); + json_t * isFollower = json_object_get(json, "isFollower"); + json_t * isInChat = json_object_get(json, "inChat"); + json_t * isMod = json_object_get(json, "isMod"); + + 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; + return member; + } + }; + + /** + * 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. + */ + 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 void Join(); + static void Leave(); + static void GetFollowers(); + static void GetMessages(); + static void ParseFollowers(); + static void ParseMessages(); + static bool ShouldTrackMember(const AudienceMember * member); + static bool ShouldMemberBeGuest(const AudienceMember * member); + static void ManageGuestNames(List &members); + static void ParseChatMessage(const char * message); + static void DoChatMessageNews(const char * message); + + static bool IsTwitchEnabled() + { + if (!gTwitchEnable) return false; + if (RCT2_GLOBAL(RCT2_ADDRESS_SCREEN_FLAGS, uint8) & (~SCREEN_FLAGS_PLAYING)) return false; + if (String::IsNullOrEmpty(gConfigTwitch.channel)) return false; + return true; + } + + static void Update() + { + if (!_twitchIdle) return; + if (IsTwitchEnabled()) { + 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 > PulseTime) + { + _twitchLastPulseTick = currentTime; + Join(); + } + break; + } + case TWITCH_STATE_JOINED: + { + uint32 currentTime = SDL_GetTicks(); + uint32 timeSinceLastPulse = currentTime - _twitchLastPulseTick; + if (_twitchLastPulseTick == 0 || timeSinceLastPulse > PulseTime) { + _twitchLastPulseTick = currentTime; + _twitchLastPulseOperation = (_twitchLastPulseOperation + 1) % 2; + switch (_twitchLastPulseOperation + TWITCH_STATE_GET_FOLLOWERS) { + case TWITCH_STATE_GET_FOLLOWERS: + GetFollowers(); + break; + case TWITCH_STATE_GET_MESSAGES: + if (gConfigTwitch.enable_news) + { + GetMessages(); + } + break; + } + } + break; + } + case TWITCH_STATE_GET_FOLLOWERS: + ParseFollowers(); + break; + case TWITCH_STATE_GET_MESSAGES: + ParseMessages(); + break; + } + } + else + { + if (_twitchState != TWITCH_STATE_LEFT) + { + Leave(); + } + } + } + + /** + * GET /leave/:join + */ + static void Join() + { + char url[256]; + snprintf(url, sizeof(url), "%sjoin/%s", TwitchExtendedBaseUrl, gConfigTwitch.channel); + + _twitchState = TWITCH_STATE_JOINING; + _twitchIdle = false; + + http_json_request request; + request.url = url; + request.method = HTTP_METHOD_GET; + request.body = nullptr; + http_request_json_async(&request, [](http_json_response *jsonResponse) -> void + { + if (jsonResponse == nullptr) + { + _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) == TWITCH_STATUS_OK) + { + _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 Leave() + { + if (_twitchJsonResponse != nullptr) + { + http_request_json_dispose(_twitchJsonResponse); + _twitchJsonResponse = nullptr; + } + + 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]; + // snprintf(url, sizeof(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 GetFollowers() + { + char url[256]; + snprintf(url, sizeof(url), "%schannel/%s/audience", TwitchExtendedBaseUrl, gConfigTwitch.channel); + + _twitchState = TWITCH_STATE_WAITING; + _twitchIdle = false; + + http_json_request request; + request.url = url; + request.method = HTTP_METHOD_GET; + request.body = NULL; + http_request_json_async(&request, [](http_json_response * jsonResponse) -> void + { + if (jsonResponse == nullptr) + { + _twitchState = TWITCH_STATE_JOINED; + } + else + { + _twitchJsonResponse = jsonResponse; + _twitchState = TWITCH_STATE_GET_FOLLOWERS; + } + _twitchIdle = true; + }); + } + + /** + * GET /channel/:channel/messages + */ + static void GetMessages() + { + char url[256]; + snprintf(url, sizeof(url), "%schannel/%s/messages", TwitchExtendedBaseUrl, gConfigTwitch.channel); + + _twitchState = TWITCH_STATE_WAITING; + _twitchIdle = false; + + http_json_request request; + request.url = url; + request.method = HTTP_METHOD_GET; + request.body = nullptr; + http_request_json_async(&request, [](http_json_response * jsonResponse) -> void + { + if (jsonResponse == nullptr) + { + _twitchState = TWITCH_STATE_JOINED; + } + else + { + _twitchJsonResponse = jsonResponse; + _twitchState = TWITCH_STATE_GET_MESSAGES; + } + _twitchIdle = true; + }); + } + + static void ParseFollowers() + { + http_json_response *jsonResponse = _twitchJsonResponse; + if (json_is_array(jsonResponse->root)) + { + List members; + + size_t audienceCount = json_array_size(jsonResponse->root); + for (size_t i = 0; i < audienceCount; i++) + { + json_t * jsonAudienceMember = json_array_get(jsonResponse->root, i); + auto member = AudienceMember::FromJson(jsonAudienceMember); + if (!String::IsNullOrEmpty(member.Name)) + { + member.ShouldTrack = ShouldTrackMember(&member); + if (ShouldMemberBeGuest(&member)) + { + members.Add(member); + } + } + } + + ManageGuestNames(members); + } + + http_request_json_dispose(_twitchJsonResponse); + _twitchJsonResponse = NULL; + _twitchState = TWITCH_STATE_JOINED; + + gfx_invalidate_screen(); + } + + static void ParseMessages() + { + http_json_response * jsonResponse = _twitchJsonResponse; + if (json_is_array(jsonResponse->root)) + { + size_t messageCount = json_array_size(jsonResponse->root); + for (size_t 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); + ParseChatMessage(text); + } + } + + http_request_json_dispose(_twitchJsonResponse); + _twitchJsonResponse = nullptr; + _twitchState = TWITCH_STATE_JOINED; + } + + static bool ShouldTrackMember(const AudienceMember * member) + { + if (member->IsInChat && gConfigTwitch.enable_chat_peep_tracking) + { + return true; + } + else if (member->IsFollower && gConfigTwitch.enable_follower_peep_tracking) + { + return true; + } + return false; + } + + static bool ShouldMemberBeGuest(const AudienceMember * member) + { + if (gConfigTwitch.enable_chat_peep_names && member->IsInChat) + { + return true; + } + else if (gConfigTwitch.enable_follower_peep_names && member->IsFollower) + { + return true; + } + return false; + } + + static void ManageGuestNames(List &members) + { + // Check what followers are already in the park + uint16 spriteIndex; + rct_peep *peep; + FOR_ALL_GUESTS(spriteIndex, peep) + { + if (is_user_string_id(peep->name_string_idx)) + { + utf8 buffer[256]; + format_string(buffer, peep->name_string_idx, NULL); + + AudienceMember * member = nullptr; + for (AudienceMember &member : members) + { + if (String::Equals(buffer, member.Name, true)) + { + member.Exists = true; + break; + } + } + + if (peep->peep_flags & PEEP_FLAGS_TWITCH) + { + if (member == nullptr) + { + // Member no longer peep name worthy + peep->peep_flags &= ~(PEEP_FLAGS_TRACKING | PEEP_FLAGS_TWITCH); + + // TODO set peep name back to number / real name + } + else + { + if (member->ShouldTrack) + { + peep->peep_flags |= (PEEP_FLAGS_TRACKING); + } + else if (!member->ShouldTrack) + { + peep->peep_flags &= ~(PEEP_FLAGS_TRACKING); + } + } + } + else if (member != nullptr && !(peep->peep_flags & PEEP_FLAGS_LEAVING_PARK)) + { + // Peep with same name already exists but not twitch + peep->peep_flags |= PEEP_FLAGS_TWITCH; + if (member->ShouldTrack) + { + peep->peep_flags |= PEEP_FLAGS_TRACKING; + } + } + } + } + + // Rename non-named peeps to followers that aren't currently in the park. + if (members.GetCount() > 0) + { + size_t memberIndex = SIZE_MAX; + FOR_ALL_GUESTS(spriteIndex, peep) + { + int originalMemberIndex = memberIndex; + for (size_t i = memberIndex + 1; i < members.GetCount(); 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->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->peep_flags |= PEEP_FLAGS_TWITCH; + if (member->ShouldTrack) + { + peep->peep_flags |= PEEP_FLAGS_TRACKING; + } + } + } + else + { + // Peep still yet to be found for member + memberIndex--; + } + } + } + } + + /** + * Like strchr but allows searching for one of many characters. + */ + static char * strchrm(const char * str, const char * find) + { + const char * result = nullptr; + do + { + const char * fch = find; + while (*fch != '\0') + { + if (*str == *fch) + { + return (char *)str; + } + fch++; + } + } + while (*str++ != '\0'); + return nullptr; + } + + static char * strskipwhitespace(const char * str) + { + while (*str == ' ' || *str == '\t') + { + str++; + } + return (char *)str; + } + + static void ParseChatMessage(const char * message) + { + message = strskipwhitespace(message); + if (!String::StartsWith(message, "!")) + { + return; + } + + // Skip '!' + message++; + + // Set buffer to the next word / token and skip + char buffer[32]; + const char * ch = strchrm(message, " \t"); + safe_strcpy(buffer, message, Math::Min(sizeof(buffer), (size_t)(ch - message + 1))); + ch = strskipwhitespace(ch); + + // Check what the word / token is + if (String::Equals(buffer, "news", true)) + { + DoChatMessageNews(ch); + } + } + + static void DoChatMessageNews(const char * message) + { + if (gConfigTwitch.enable_news) + { + utf8 buffer[256]; + buffer[0] = (utf8)FORMAT_TOPAZ; + safe_strcpy(buffer + 1, message, sizeof(buffer) - 1); + + // Remove unsupport characters + // TODO allow when OpenRCT2 gains unicode support + char * ch = buffer + 1; + 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); + } + } +} 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_json_request request; - request.url = url; - request.method = HTTP_METHOD_GET; - request.body = NULL; - http_request_json_async(&request, [](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_json_request request; - request.url = url; - request.method = HTTP_METHOD_GET; - request.body = NULL; - http_request_json_async(&request, [](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_json_request request; - request.url = url; - request.method = HTTP_METHOD_GET; - request.body = NULL; - http_request_json_async(&request, [](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->peep_flags & PEEP_FLAGS_TWITCH) { - if (member == NULL) { - // Member no longer peep name worthy - peep->peep_flags &= ~(PEEP_FLAGS_TRACKING | PEEP_FLAGS_TWITCH); - - // TODO set peep name back to number / real name - } else { - if (member->shouldTrack) - peep->peep_flags |= (PEEP_FLAGS_TRACKING); - else if (!member->shouldTrack) - peep->peep_flags &= ~(PEEP_FLAGS_TRACKING); - } - } else if (member != NULL && !(peep->peep_flags & PEEP_FLAGS_LEAVING_PARK)) { - // Peep with same name already exists but not twitch - peep->peep_flags |= PEEP_FLAGS_TWITCH; - if (member->shouldTrack) - peep->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->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->peep_flags |= PEEP_FLAGS_TWITCH; - if (member->shouldTrack) - peep->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"); - safe_strcpy(buffer, message, ch - message + 1); - if (_strcmpi(buffer, "news") == 0) { - if (gConfigTwitch.enable_news) { - ch = strskipwhitespace(ch); - - buffer[0] = (char)FORMAT_TOPAZ; - safe_strcpy(buffer + 1, ch, sizeof(buffer) - 2); - - // 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); - } - } + Twitch::Update(); } #endif