OpenRCT2/src/openrct2/network/Twitch.cpp

575 lines
17 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2018 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#ifdef DISABLE_TWITCH
# include "twitch.h"
void twitch_update()
{
}
#else
# ifdef DISABLE_HTTP
# error HTTP must be enabled to use the TWITCH functionality.
# endif
# include "../Context.h"
# include "../Game.h"
# include "../OpenRCT2.h"
# include "../config/Config.h"
# include "../core/Json.hpp"
# include "../core/Math.hpp"
# include "../core/String.hpp"
# include "../drawing/Drawing.h"
# include "../interface/InteractiveConsole.h"
# include "../localisation/Localisation.h"
# include "../management/NewsItem.h"
# include "../peep/Peep.h"
# include "../platform/platform.h"
# include "../util/Util.h"
# include "../world/Sprite.h"
# include "Http.h"
# include "twitch.h"
# include <jansson.h>
# include <memory>
# include <vector>
using namespace OpenRCT2;
using namespace OpenRCT2::Network;
bool gTwitchEnable = false;
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
};
enum
{
TWITCH_STATUS_OK = 200
};
struct AudienceMember
{
const char* Name;
bool IsFollower;
bool IsInChat;
bool IsMod;
bool Exists;
bool ShouldTrack;
static AudienceMember FromJson(json_t* json)
{
AudienceMember member = {};
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_is_true(isFollower);
member.IsInChat = json_is_true(isInChat);
member.IsMod = json_is_true(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_t PulseTime = 10 * 1000;
static int32_t _twitchState = TWITCH_STATE_LEFT;
static bool _twitchIdle = true;
static uint32_t _twitchLastPulseTick = 0;
static int32_t _twitchLastPulseOperation = 1;
static Http::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(std::vector<AudienceMember>& members);
static void ParseChatMessage(const char* message);
static void DoChatMessageNews(const char* message);
static bool IsTwitchEnabled()
{
if (!gTwitchEnable)
return false;
if (gScreenFlags & (~SCREEN_FLAGS_PLAYING))
return false;
if (String::IsNullOrEmpty(gConfigTwitch.channel))
return false;
return true;
}
static void Update()
{
if (!_twitchIdle)
return;
if (IsTwitchEnabled())
{
if (game_is_paused())
return;
switch (_twitchState)
{
case TWITCH_STATE_LEFT:
{
uint32_t currentTime = platform_get_ticks();
uint32_t timeSinceLastPulse = currentTime - _twitchLastPulseTick;
if (_twitchLastPulseTick == 0 || timeSinceLastPulse > PulseTime)
{
_twitchLastPulseTick = currentTime;
Join();
}
break;
}
case TWITCH_STATE_JOINED:
{
uint32_t currentTime = platform_get_ticks();
uint32_t 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];
if (gConfigTwitch.api_url == nullptr || strlen(gConfigTwitch.api_url) == 0)
{
auto context = GetContext();
context->WriteLine("API URL is empty! skipping request...");
return;
}
snprintf(url, sizeof(url), "%s/join/%s", gConfigTwitch.api_url, gConfigTwitch.channel);
_twitchState = TWITCH_STATE_JOINING;
_twitchIdle = false;
Http::Request request;
request.url = url;
request.method = Http::Method::GET;
Http::DoAsync(request, [](Http::Response res) {
std::shared_ptr<void> _(nullptr, [&](...) { _twitchIdle = true; });
if (res.status != Http::Status::OK)
{
_twitchState = TWITCH_STATE_LEFT;
GetContext()->WriteLine("Unable to connect to twitch channel.");
return;
}
auto root = Json::FromString(res.body);
json_t* jsonStatus = json_object_get(root, "status");
if (json_is_number(jsonStatus) && json_integer_value(jsonStatus) == TWITCH_STATUS_OK)
{
_twitchState = TWITCH_STATE_JOINED;
}
else
{
_twitchState = TWITCH_STATE_LEFT;
}
_twitchLastPulseTick = 0;
GetContext()->WriteLine("Connected to twitch channel.");
});
}
/**
* GET /leave/:channel
*/
static void Leave()
{
GetContext()->WriteLine("Left twitch channel.");
_twitchJsonResponse = {};
_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;
//
// GetContext()->WriteLine("Left twitch channel.");
// });
}
/**
* GET /channel/:channel/audience
*/
static void GetFollowers()
{
char url[256];
if (gConfigTwitch.api_url == nullptr || strlen(gConfigTwitch.api_url) == 0)
{
auto context = GetContext();
context->WriteLine("API URL is empty! skipping request...");
return;
}
snprintf(url, sizeof(url), "%s/channel/%s/audience", gConfigTwitch.api_url, gConfigTwitch.channel);
_twitchState = TWITCH_STATE_WAITING;
_twitchIdle = false;
Http::DoAsync({ url }, [](Http::Response res) {
std::shared_ptr<void> _(nullptr, [&](...) { _twitchIdle = true; });
if (res.status != Http::Status::OK)
{
_twitchState = TWITCH_STATE_JOINED;
return;
}
_twitchJsonResponse = res;
_twitchState = TWITCH_STATE_GET_FOLLOWERS;
});
}
/**
* GET /channel/:channel/messages
*/
static void GetMessages()
{
char url[256];
if (gConfigTwitch.api_url == nullptr || strlen(gConfigTwitch.api_url) == 0)
{
auto context = GetContext();
context->WriteLine("API URL is empty! skipping request...");
return;
}
snprintf(url, sizeof(url), "%s/channel/%s/messages", gConfigTwitch.api_url, gConfigTwitch.channel);
_twitchState = TWITCH_STATE_WAITING;
_twitchIdle = false;
Http::DoAsync({ url }, [](Http::Response res) {
std::shared_ptr<void> _(nullptr, [&](...) { _twitchIdle = true; });
if (res.status != Http::Status::OK)
{
_twitchState = TWITCH_STATE_JOINED;
return;
}
_twitchJsonResponse = res;
_twitchState = TWITCH_STATE_GET_MESSAGES;
});
}
static void ParseFollowers()
{
json_t* root = Json::FromString(_twitchJsonResponse.body);
if (json_is_array(root))
{
std::vector<AudienceMember> members;
size_t audienceCount = json_array_size(root);
for (size_t i = 0; i < audienceCount; i++)
{
json_t* jsonAudienceMember = json_array_get(root, i);
auto member = AudienceMember::FromJson(jsonAudienceMember);
if (!String::IsNullOrEmpty(member.Name))
{
member.ShouldTrack = ShouldTrackMember(&member);
if (ShouldMemberBeGuest(&member))
{
members.push_back(member);
}
}
}
ManageGuestNames(members);
}
_twitchJsonResponse = {};
_twitchState = TWITCH_STATE_JOINED;
gfx_invalidate_screen();
}
static void ParseMessages()
{
json_t* root = Json::FromString(_twitchJsonResponse.body);
if (json_is_array(root))
{
size_t messageCount = json_array_size(root);
for (size_t i = 0; i < messageCount; i++)
{
json_t* jsonMessage = json_array_get(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);
}
}
_twitchJsonResponse = {};
_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(std::vector<AudienceMember>& members)
{
// Check what followers are already in the park
uint16_t spriteIndex;
rct_peep* peep;
FOR_ALL_GUESTS (spriteIndex, peep)
{
if (is_user_string_id(peep->name_string_idx))
{
utf8 buffer[256];
format_string(buffer, 256, peep->name_string_idx, nullptr);
AudienceMember* member = nullptr;
for (AudienceMember& m : members)
{
if (String::Equals(buffer, m.Name, true))
{
member = &m;
m.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.empty())
{
size_t memberIndex = SIZE_MAX;
FOR_ALL_GUESTS (spriteIndex, peep)
{
size_t 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(USER_STRING_HIGH_ID_NUMBER, 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--;
}
}
}
}
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++;
// Check that command is "news"
const char *ch, *cmd;
for (ch = message, cmd = "news"; *cmd != '\0'; ++ch, ++cmd)
{
if (*ch != *cmd)
return;
}
if (!isspace(*ch))
return;
ch = strskipwhitespace(ch);
DoChatMessageNews(ch);
}
static void DoChatMessageNews(const char* message)
{
if (gConfigTwitch.enable_news)
{
utf8 buffer[256];
buffer[0] = (utf8)(uint8_t)FORMAT_TOPAZ;
safe_strcpy(buffer + 1, message, sizeof(buffer) - 1);
utf8_remove_formatting(buffer, false);
// TODO Create a new news item type for twitch which has twitch icon
news_item_add_to_queue_raw(NEWS_ITEM_BLANK, buffer, 0);
}
}
} // namespace Twitch
void twitch_update()
{
Twitch::Update();
}
#endif