mirror of https://github.com/OpenRCT2/OpenRCT2.git
add twitch integration, part 1
This commit is contained in:
parent
b26e546171
commit
1ac93e44c9
13
src/config.c
13
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);
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include <stdarg.h>
|
||||
#include <SDL_scancode.h>
|
||||
|
||||
#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 <objectfilenodat>" },
|
||||
{ "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) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define _CONSOLE_H_
|
||||
|
||||
#include "../common.h"
|
||||
#include "../drawing/drawing.h"
|
||||
|
||||
extern bool gConsoleOpen;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
extern "C" {
|
||||
#include "http.h"
|
||||
}
|
||||
|
||||
#ifdef DISABLE_HTTP
|
||||
|
||||
void http_init() { }
|
||||
void http_dispose() { }
|
||||
|
||||
#else
|
||||
|
||||
#include <SDL.h>
|
||||
#include <curl/curl.h>
|
||||
#include <jansson/jansson.h>
|
||||
|
||||
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
|
|
@ -0,0 +1,18 @@
|
|||
#ifndef _HTTP_H_
|
||||
#define _HTTP_H_
|
||||
|
||||
#include <jansson/jansson.h>
|
||||
#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
|
|
@ -0,0 +1,435 @@
|
|||
#ifdef DISABLE_TWITCH
|
||||
|
||||
extern "C" {
|
||||
#include "twitch.h"
|
||||
}
|
||||
|
||||
void twitch_update() { }
|
||||
|
||||
#else
|
||||
|
||||
// REQUIRES HTTP
|
||||
|
||||
#include <vector>
|
||||
#include <SDL.h>
|
||||
|
||||
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<AudienceMember> 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
|
|
@ -0,0 +1,10 @@
|
|||
#ifndef _TWITCH_H_
|
||||
#define _TWITCH_H_
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern bool gTwitchEnable;
|
||||
|
||||
void twitch_update();
|
||||
|
||||
#endif
|
|
@ -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!
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue