diff --git a/APIState.h b/APIState.h new file mode 100644 index 0000000..c802a08 --- /dev/null +++ b/APIState.h @@ -0,0 +1,15 @@ +#pragma once + +struct player { + const char* name; + const char* type; + const char* race; + const char* result; +}; + +struct APIState { + std::vector activeScreens; + bool isReplay; + double displayTime; + std::vector players; +}; \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt old mode 100644 new mode 100755 index fb20734..d34a010 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,41 +1,53 @@ -project(sc2switcher) - +project(sc2switcher3) include_directories(${OBS_JANSSON_INCLUDE_DIRS}) -set(sc2switcher_HEADERS - ${sc2switcher_HEADERS} - auto-scene-switcher.hpp - tool-helpers.hpp +set(sc2switcher3_HEADERS + ${sc2switcher3_HEADERS} + SC2Data.h + Observer.h + SC2State.h + Constants.h + SceneSwitcher.h + ScoreTracker.h + Webhook.h + Config.h + forms/SettingsDialog.h ) -set(sc2switcher_SOURCES - ${sc2switcher_SOURCES} - sc2switcher.c - auto-scene-switcher.cpp +set(sc2switcher3_SOURCES + ${sc2switcher3_SOURCES} + sc2switcher.cpp + SC2Data.cpp + SC2State.cpp + SceneSwitcher.cpp + ScoreTracker.cpp + Webhook.cpp + Config.cpp + forms/SettingsDialog.cpp ) -set(sc2switcher_UI - ${sc2switcher_UI} - forms/auto-scene-switcher.ui +set(sc2switcher3_UI + ${sc2switcher3_UI} + forms/SettingsDialog.ui ) -set(sc2switcher_PLATFORM_LIBS +set(sc2switcher3_PLATFORM_LIBS ${OBS_JANSSON_IMPORT} ) -qt5_wrap_ui(sc2switcher_UI_HEADERS - ${sc2switcher_UI} - ${sc2switcher_PLATFORM_UI}) +qt5_wrap_ui(sc2switcher3_UI_HEADERS + ${sc2switcher3_UI} + ${sc2switcher3_PLATFORM_UI}) -add_library(sc2switcher MODULE - ${sc2switcher_HEADERS} - ${sc2switcher_SOURCES} - ${sc2switcher_UI_HEADERS} - ${sc2switcher_PLATFORM_SOURCES} - ${sc2switcher_PLATFORM_HEADERS} +add_library(sc2switcher3 MODULE + ${sc2switcher3_HEADERS} + ${sc2switcher3_SOURCES} + ${sc2switcher3_UI_HEADERS} + ${sc2switcher3_PLATFORM_SOURCES} + ${sc2switcher3_PLATFORM_HEADERS} ) -target_link_libraries(sc2switcher - ${sc2switcher_PLATFORM_LIBS} +target_link_libraries(sc2switcher3 + ${sc2switcher3_PLATFORM_LIBS} obs-frontend-api Qt5::Widgets - libobs - ${LIBCURL_LIBRARIES}) + ${LIBCURL_LIBRARIES} + libobs) -install_obs_plugin(sc2switcher) +install_obs_plugin(sc2switcher3) diff --git a/Config.cpp b/Config.cpp new file mode 100644 index 0000000..0315ebb --- /dev/null +++ b/Config.cpp @@ -0,0 +1,74 @@ +#include +#include +#include "Config.h" + +Config* Config::_instance = new Config(); + +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +Config::Config() : + ipAddr(std::string("localhost")), + scoreString(std::string("vT: ${tw}-${tl}\nvZ: ${zw}-${zl}\nvP: ${pw}-${pl}")), + isRunning(false), + switcherEnabled(false), + scoresEnabled(false), + popupsEnabled(true) {} + +Config* Config::Current() { + return _instance; +} + +Config::~Config() { + delete _instance; +} + +void Config::checkForUpdates() { + CURL *curl; + CURLcode res; + std::string response; + + curl = curl_easy_init(); + if (curl) { + std::string reqURL = "https://api.github.com/repos/leigholiver/OBS-SC2Switcher/releases/latest"; + + struct curl_slist *chunk = NULL; + + curl_easy_setopt(curl, CURLOPT_URL, reqURL.c_str()); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 500); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + chunk = curl_slist_append(chunk, "User-Agent: OBS-SC2Switcher"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + + res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + if (res != CURLE_OK) { + return; + } + } + + json_error_t error; + json_t* root = json_loads(response.c_str(), 0, &error); + if (!root) { + return; + } + + json_t* url = json_object_get(root, "tag_name"); + const char *urlText = json_string_value(url); + float latestVer = stof(urlText); + float currentVer = 0.9; + if(latestVer > currentVer) { + json_t* url2 = json_object_get(root, "html_url"); + const char *urlText2 = json_string_value(url2); + updateURL = urlText2; + + json_t* patch = json_object_get(root, "body"); + const char *patchText = json_string_value(patch); + updateDescription = patchText; + } +} \ No newline at end of file diff --git a/Config.h b/Config.h new file mode 100644 index 0000000..aba8eae --- /dev/null +++ b/Config.h @@ -0,0 +1,169 @@ + +#pragma once +#include +#include +#include + +#include "Constants.h" +#include "obs-util.h" +#include + +using namespace std; + +class Config { + public: + Config(); + ~Config(); + static Config* Current(); + + // sc2data + std::string ipAddr; + vector usernames; + vector recentUsernames; + + // scene switcher + OBSWeakSource inGameScene; + OBSWeakSource outGameScene; + OBSWeakSource replayScene; + OBSWeakSource obsScene; + OBSWeakSource menuScenes[10]; + + // score tracker + std::string textSourceName; + std::string scoreString; + + bool isRunning; + + std::string updateURL; + std::string updateDescription; + void checkForUpdates(); + + bool switcherEnabled; + bool scoresEnabled; + bool popupsEnabled; + + bool webhookEnabled; + + vector webhookURLList; + + private: + static Config* _instance; +}; + + +static void LoadSaveHandler(obs_data_t *save_data, bool saving, void *) { + Config* cfg = Config::Current(); + + if (saving) { + obs_data_t *obj = obs_data_create(); + + // settings + obs_data_set_bool(obj, "is_running", cfg->isRunning); + obs_data_set_bool(obj, "switcher_enabled", cfg->switcherEnabled); + obs_data_set_bool(obj, "scores_enabled", cfg->scoresEnabled); + obs_data_set_bool(obj, "popups_enabled", cfg->popupsEnabled); + obs_data_set_string(obj, "ip_addr", cfg->ipAddr.c_str()); + obs_data_set_bool(obj, "webhook_enabled", cfg->webhookEnabled); + + obs_data_array_t *array = obs_data_array_create(); + for (string &s : cfg->webhookURLList) { + obs_data_t *array_obj = obs_data_create(); + obs_data_set_string(array_obj, "URL", s.c_str()); + obs_data_array_push_back(array, array_obj); + obs_data_release(array_obj); + } + obs_data_set_array(obj, "webhookURLs", array); + obs_data_array_release(array); + + obs_data_array_t *array2 = obs_data_array_create(); + for (string &s : cfg->usernames) { + obs_data_t *array_obj = obs_data_create(); + obs_data_set_string(array_obj, "username", s.c_str()); + obs_data_array_push_back(array2, array_obj); + obs_data_release(array_obj); + } + obs_data_set_array(obj, "usernames", array2); + obs_data_array_release(array2); + + obs_data_set_string(obj, "textSourceName", cfg->textSourceName.c_str()); + obs_data_set_string(obj, "scoreString", cfg->scoreString.c_str()); + + // scenes + obs_data_set_string(obj, "in_game_scene", GetWeakSourceName(cfg->inGameScene).c_str()); + obs_data_set_string(obj, "out_game_scene", GetWeakSourceName(cfg->outGameScene).c_str()); + obs_data_set_string(obj, "replay_scene", GetWeakSourceName(cfg->replayScene).c_str()); + obs_data_set_string(obj, "obs_scene", GetWeakSourceName(cfg->obsScene).c_str()); + obs_data_set_string(obj, "MENU_SCORESCREEN", GetWeakSourceName(cfg->menuScenes[MENU_SCORESCREEN]).c_str()); + obs_data_set_string(obj, "MENU_PROFILE", GetWeakSourceName(cfg->menuScenes[MENU_PROFILE]).c_str()); + obs_data_set_string(obj, "MENU_LOBBY", GetWeakSourceName(cfg->menuScenes[MENU_LOBBY]).c_str()); + obs_data_set_string(obj, "MENU_HOME", GetWeakSourceName(cfg->menuScenes[MENU_HOME]).c_str()); + obs_data_set_string(obj, "MENU_CAMPAIGN", GetWeakSourceName(cfg->menuScenes[MENU_CAMPAIGN]).c_str()); + obs_data_set_string(obj, "MENU_COLLECTION", GetWeakSourceName(cfg->menuScenes[MENU_COLLECTION]).c_str()); + obs_data_set_string(obj, "MENU_COOP", GetWeakSourceName(cfg->menuScenes[MENU_COOP]).c_str()); + obs_data_set_string(obj, "MENU_CUSTOM", GetWeakSourceName(cfg->menuScenes[MENU_CUSTOM]).c_str()); + obs_data_set_string(obj, "MENU_REPLAYS", GetWeakSourceName(cfg->menuScenes[MENU_REPLAYS]).c_str()); + obs_data_set_string(obj, "MENU_VERSUS", GetWeakSourceName(cfg->menuScenes[MENU_VERSUS]).c_str()); + + + obs_data_set_obj(save_data, "sc2switcher2", obj); + obs_data_release(obj); + } + else { + obs_data_t *obj = obs_data_get_obj(save_data, "sc2switcher2"); + if (!obj) { + obj = obs_data_create(); + } + cfg->ipAddr = obs_data_get_string(obj, "ip_addr"); + + obs_data_array_t *array = obs_data_get_array(obj, "usernames"); + size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + obs_data_t *array_obj = obs_data_array_item(array, i); + cfg->usernames.push_back(obs_data_get_string(array_obj, "username")); + obs_data_release(array_obj); + } + obs_data_array_release(array); + + obs_data_array_t *array2 = obs_data_get_array(obj, "webhookURLs"); + size_t count2 = obs_data_array_count(array2); + for (size_t i = 0; i < count2; i++) { + obs_data_t *array_obj = obs_data_array_item(array2, i); + cfg->webhookURLList.push_back(obs_data_get_string(array_obj, "URL")); + obs_data_release(array_obj); + } + obs_data_array_release(array2); + + cfg->textSourceName = obs_data_get_string(obj, "textSourceName"); + + + string scoreString = obs_data_get_string(obj, "scoreString"); + if (scoreString != "") { + cfg->scoreString = scoreString; + } + + cfg->isRunning = obs_data_get_bool(obj, "is_running"); + cfg->switcherEnabled = obs_data_get_bool(obj, "switcher_enabled"); + cfg->scoresEnabled = obs_data_get_bool(obj, "scores_enabled"); + cfg->popupsEnabled = obs_data_get_bool(obj, "popups_enabled"); + + cfg->inGameScene = GetWeakSourceByName(obs_data_get_string(obj, "in_game_scene")); + cfg->outGameScene = GetWeakSourceByName(obs_data_get_string(obj, "out_game_scene")); + cfg->replayScene = GetWeakSourceByName(obs_data_get_string(obj, "replay_scene")); + cfg->obsScene = GetWeakSourceByName(obs_data_get_string(obj, "obs_scene")); + cfg->menuScenes[MENU_SCORESCREEN] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_SCORESCREEN")); + cfg->menuScenes[MENU_PROFILE] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_PROFILE")); + cfg->menuScenes[MENU_LOBBY] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_LOBBY")); + cfg->menuScenes[MENU_HOME] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_HOME")); + cfg->menuScenes[MENU_CAMPAIGN] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_CAMPAIGN")); + cfg->menuScenes[MENU_COLLECTION] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_COLLECTION")); + cfg->menuScenes[MENU_COOP] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_COOP")); + cfg->menuScenes[MENU_CUSTOM] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_CUSTOM")); + cfg->menuScenes[MENU_REPLAYS] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_REPLAYS")); + cfg->menuScenes[MENU_VERSUS] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_VERSUS")); + + cfg->webhookEnabled = obs_data_get_bool(obj, "webhook_enabled"); + + obs_data_release(obj); + + } +} diff --git a/Constants.h b/Constants.h new file mode 100644 index 0000000..7c6d9bd --- /dev/null +++ b/Constants.h @@ -0,0 +1,39 @@ +#pragma once + +// the order is important with these as the labels are not +// in a consistent order in sc2. the scene switcher will +// use whichever it comes accross first. menu consts must +// go first so that they match up to the menuLabels array +// properly. this is kind of bad? +const int MENU_SCORESCREEN = 0; +const int MENU_PROFILE = 1; +const int MENU_LOBBY = 2; +const int MENU_HOME = 3; +const int MENU_CAMPAIGN = 4; +const int MENU_COLLECTION = 5; +const int MENU_COOP = 6; +const int MENU_CUSTOM = 7; +const int MENU_REPLAYS = 8; +const int MENU_VERSUS = 9; +const int MENU_NONE = 10; + +const char* const menuLabels[] = { + "ScreenScore/ScreenScore", + "ScreenUserProfile/ScreenUserProfile", + "ScreenBattleLobby/ScreenBattleLobby", + "ScreenHome/ScreenHome", + "ScreenSingle/ScreenSingle", + "ScreenCollection/ScreenCollection", + "ScreenCoopCampaign/ScreenCoopCampaign", + "ScreenCustom/ScreenCustom", + "ScreenReplay/ScreenReplay", + "ScreenMultiplayer/ScreenMultiplayer", +}; + +const int APP_INGAME = 11; +const int APP_MENU = 12; +const int APP_LOADING = 13; + +const int GAME_INGAME = 14; +const int GAME_OBS = 15; +const int GAME_REPLAY = 16; diff --git a/Observer.h b/Observer.h new file mode 100644 index 0000000..e0d91b8 --- /dev/null +++ b/Observer.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "SC2Data.h" +#include "SC2State.h" + + +#include + +class Observer : public QObject { + Q_OBJECT + SC2Data *sc2; + + public: + Observer(SC2Data *scdata, QObject* parent = Q_NULLPTR) + : QObject(parent) { + sc2 = scdata; + sc2->attach(this); + } + virtual void notify(SC2State*& previous, SC2State*& current) = 0; +}; \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index d8652cb..74c2cde --- a/README.md +++ b/README.md @@ -1,28 +1,70 @@ -## OBS SC2 Scene Switcher +## OBS SC2Switcher -## [Download](https://github.com/leigholiver/OBS-SC2Switcher/releases/download/0.7/sc2switcher.zip) +## [Download](https://github.com/leigholiver/OBS-SC2Switcher/releases/latest/) ## Installation: -64bit: copy `sc2switcher.dll` from the `obs-plugins/64bit` folder in the zip file into `c:/program files (x86)/obs-studio/obs-plugins/64bit` directory +### Windows +64bit: copy `sc2switcher.dll` from the zip file into `c:/program files (x86)/obs-studio/obs-plugins/64bit` directory -32bit: copy `sc2switcher-32.dll` from the `obs-plugins/32bit` folder in the zip file into `c:/program files (x86)/obs-studio/obs-plugins/32bit` directory +32bit: copy `sc2switcher-32.dll` from the zip file into `c:/program files (x86)/obs-studio/obs-plugins/32bit` directory -### Usage: -Go to Tools -> SC2 Scene Switcher and select a scene to switch to for in game/out of game/in replay. +### Linux +Copy `sc2switcher.so` from the zip file into your obs-plugins directory. For me this is `/usr/lib/obs-plugins/` but this may vary depending on your distro -#### If you use a separate PC to stream: +## Usage: +In the Tools menu, click on SC2Switcher. + +### Scene Switcher +- Choose a scene to switch to in different menus/games +- All are optional and will fall back to the in game/out of game scenes +- The observing scene requires you to have entered your username in the 'usernames' tab + +### Score Tracker +- Enter the name of the text source you are using for your scores +- The text source will be updated with your score (mostly) automatically +- If you play against a random player the plugin will ask you for their race +- If you are neither player or both players (ie barcodes), the plugin will ask which player you were +- There is a small chance that these notifications will take focus over sc2. If this is an issue you can untick 'Popups Enabled' and these will be ignored. You can use the buttons to update the score manually + + +### Game Webhook +- When entering or leaving a game, the plugin will send a request to the specified url with information about the game for use in other applications +- For an example of how this could be used you could check out [Ladderbet](https://github.com/leigholiver/ladderbet/), an automated twitch chat betting bot + +``` +event: 'enter' or 'exit', +displayTime: ~, +players: [ + { + 'name': ~, + 'type': ~, + 'race': 'Terr', 'Zerg' or 'Prot', + 'result': 'Victory' or 'Defeat', + 'isme': 'true' or 'false', + }, +] +scores: { + 'Terr', 'Zerg' or 'Prot': { + "Victory": ~, + "Defeat": ~ + }, +} +``` +- Known issue: Scores may not update until the start of the next game if you change the score manually or come across one of the situations mentioned in the Score Tracker section + +## If you use a separate PC to stream: Enter the IP address of your SC2 computer in the SC2 PC IP box. On your SC2 PC, open the Battle.net launcher, click Options, Game Settings, and under SC2, check 'Additional Command Line Arguments', and enter `-clientapi 6119` into the text box. You can check that SC2 is configured correctly by going to `http://[Your SC2 PC IP]:6119/ui` in your browser on the streaming PC. It should look something like: `{"activeScreens":["ScreenBackgroundSC2/ScreenBackgroundSC2","ScreenReplay/ScreenReplay","ScreenNavigationSC2/ScreenNavigationSC2","ScreenForegroundSC2/ScreenForegroundSC2","ScreenBattlenet/ScreenBattlenet"]}`. -#### Building from source: +## Building from source: Make sure you have a version of obs-studio building properly [(instructions are here)](https://github.com/jp9000/obs-studio/wiki/Install-Instructions). Clone this repository into `[OBS Source Directory]/UI/frontend-plugins/SC2Switcher` Add the line `add_subdirectory(SC2Switcher)` to `[OBS Source Directory]/UI/frontend-plugins/CMakeLists.txt` -Run CMake again and hit Configure then Generate +Run CMake again and hit Configure, Generate, then Open Project diff --git a/SC2Data.cpp b/SC2Data.cpp new file mode 100644 index 0000000..687e1e4 --- /dev/null +++ b/SC2Data.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#include + +#include "SC2Data.h" +#include "SC2State.h" +#include "Observer.h" +#include "Config.h" + +QT_USE_NAMESPACE + +SC2Data* SC2Data::Instance = nullptr; + +SC2Data::SC2Data(QObject* parent) + : QObject(parent) +{ + timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), this, SLOT(update())); + timer->start(1000); + state = nullptr; +} + +SC2Data::~SC2Data() { + delete Instance; +} + +void SC2Data::attach(Observer *obs) { + watchers.push_back(obs); +} + +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +void SC2Data::update() { + Config* config = Config::Current(); + if(config->isRunning) { + CURL *curl; + CURLcode res; + std::string reqURL; + std::string UIResponse; + std::string gameResponse; + + reqURL = config->ipAddr; + if(reqURL == "") { + reqURL = "localhost"; + } + reqURL = "http://" + reqURL + ":6119/"; + + curl = curl_easy_init(); + if (curl) { + std::string url = reqURL + "ui"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 50); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &UIResponse); + res = curl_easy_perform(curl); + //curl_easy_cleanup(curl); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + return; + } + } + + curl_easy_reset(curl); + if (curl) { + std::string url = reqURL + "game"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 50); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &gameResponse); + res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + if (res != CURLE_OK) { + return; + } + } + + // create an sc2state object + SC2State* newState = new SC2State(); + newState->fromJSONString(UIResponse, gameResponse); + if(state == nullptr) { + state = newState; + } + + if(state->appState != newState->appState || + state->gameState != newState->gameState || + state->menuState != newState->menuState ) { + for (size_t i = 0; i < watchers.size(); i++) { + watchers[i]->notify(state, newState); + } + state = newState; + } + } +} \ No newline at end of file diff --git a/SC2Data.h b/SC2Data.h new file mode 100644 index 0000000..03df10a --- /dev/null +++ b/SC2Data.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +#include "SC2State.h" + +using std::vector; + +class Observer; +class SC2Data : public QObject { + Q_OBJECT + + private: + SC2State* state; + QTimer* timer; + vector watchers; + //QNetworkAccessManager networkManager; + //QUrl uiUrl; + //QUrl gameUrl; + + public: + explicit SC2Data(QObject* parent = Q_NULLPTR); + virtual ~SC2Data(); + static SC2Data* Instance; + void attach(Observer* obs); + + public slots: + void update(); +}; \ No newline at end of file diff --git a/SC2State.cpp b/SC2State.cpp new file mode 100644 index 0000000..f3dbd0e --- /dev/null +++ b/SC2State.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include "Constants.h" +#include "Config.h" +#include "SC2State.h" +#include "APIState.h" + +#include "obs-util.h" + +const char* jsonToString(json_t * result, const char* name) { + json_t * jsonName = json_object_get(result, name); + if (NULL != jsonName && json_is_string(jsonName)) { + return json_string_value(jsonName); + } + return nullptr; +} + + +/** +AppState (GAME/MENU/LOADING) +GameState (INGAME/OBS/REPLAY) +MenuState (SCORESCREEN/PROFILE/HOME/CAMPAIGN/COOP/REPLAYS/VERSUS/CUSTOM/LOBBY/COLLECTION) +**/ +SC2State::SC2State(QObject* parent) + : QObject(parent) +{ + +} + +SC2State::~SC2State() { +} + +void SC2State::fromJSONString(std::string uiResponse, std::string gameResponse) { + + APIState api; + + json_error_t error; + json_t* root = json_loads(uiResponse.c_str(), 0, &error); + if (!root) { + return; + } + json_t* screens = json_object_get(root, "activeScreens"); + for (size_t i = 0; i < json_array_size(screens); i++) { + json_t* obj_txt = json_array_get(screens, i);; + const char* strText; + if (NULL != obj_txt && json_is_string(obj_txt)) { + strText = json_string_value(obj_txt); + api.activeScreens.push_back(strText); + } + } + + root = json_loads(gameResponse.c_str(), 0, &error); + if (!root) { + return; + } + + json_t* isReplay = json_object_get(root, "isReplay"); + api.isReplay = (isReplay == json_true()); + + json_t* displayTime = json_object_get(root, "displayTime"); + if(json_is_number(displayTime)) { + double dp = json_real_value(displayTime); + api.displayTime = dp; + } + + + json_t* players = json_object_get(root, "players"); + for (size_t i = 0; i < json_array_size(players); i++) { + player* p = new player; + json_t* playerJSON = json_array_get(players, i); + p->name = (jsonToString(playerJSON, "name") == nullptr? "" : jsonToString(playerJSON, "name")); + p->type = (jsonToString(playerJSON, "type") == nullptr? "" : jsonToString(playerJSON, "type")); + p->race = (jsonToString(playerJSON, "race") == nullptr? "" : jsonToString(playerJSON, "race")); + p->result = (jsonToString(playerJSON, "result") == nullptr? "" : jsonToString(playerJSON, "result")); + api.players.push_back(p); + } + + fullState = api; + fillState(); +} + +void SC2State::fillState() { + Config* cfg = Config::Current(); + if (fullState.activeScreens.size() > 1) { + bool found = false; + int sceneIdx = 0; + int max = sizeof(menuLabels) / sizeof(menuLabels[0]); + + while (!found && sceneIdx < max) { + for (size_t i = 0; i < fullState.activeScreens.size(); i++) { + std::string active = fullState.activeScreens[i]; + std::string label = menuLabels[sceneIdx]; + if (active == label) { + menuState = sceneIdx; + found = true; + break; + } + } + sceneIdx++; + } + if(!found) { + menuState = MENU_NONE; + } + appState = APP_MENU; + } + else if (fullState.activeScreens.size() == 0) { + // in game + appState = APP_INGAME; + gameState = GAME_INGAME; + } + else { + // we are in the loading screen + appState = APP_LOADING; + } + + if(fullState.isReplay) { + gameState = GAME_REPLAY; + } + + bool usernameFound = false; + for (size_t i = 0; i < fullState.players.size(); i++) { + if (cfg->usernames.size() > 0) { + // if this is one of our usernames + if (std::find(cfg->usernames.begin(), cfg->usernames.end(), fullState.players[i]->name) != cfg->usernames.end()) { + usernameFound = true; + } + } + // if this username isnt already in our recents, add it + if (std::find(cfg->recentUsernames.begin(), cfg->recentUsernames.end(), fullState.players[i]->name) == cfg->recentUsernames.end()) { + cfg->recentUsernames.push_back(fullState.players[i]->name); + + // limit the size of the recent usernames list, it could get quite big + if (cfg->recentUsernames.size() > 8) { + cfg->recentUsernames.erase(cfg->recentUsernames.begin()); + } + } + } + + if (!usernameFound && cfg->usernames.size() > 0) { + gameState = GAME_OBS; + } +} + + diff --git a/SC2State.h b/SC2State.h new file mode 100644 index 0000000..535c3b6 --- /dev/null +++ b/SC2State.h @@ -0,0 +1,17 @@ +#pragma once +#include "Constants.h" +#include "APIState.h" + +class SC2State : public QObject { + Q_OBJECT + + public: + explicit SC2State(QObject* parent = Q_NULLPTR); + virtual ~SC2State(); + int appState = 0; + int gameState = 0; + int menuState = 0; + APIState fullState; + void fromJSONString(std::string uiResponse, std::string gameResponse); + void fillState(); +}; \ No newline at end of file diff --git a/SceneSwitcher.cpp b/SceneSwitcher.cpp new file mode 100644 index 0000000..75f26c1 --- /dev/null +++ b/SceneSwitcher.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include + +#include + +#include "SC2Data.h" +#include "Observer.h" +#include "SC2State.h" +#include "SceneSwitcher.h" +#include "Constants.h" +#include "Config.h" +#include "obs-util.h" + +SceneSwitcher::SceneSwitcher(SC2Data *sc2): Observer(sc2){ } + +void SceneSwitcher::notify(SC2State*& previous, SC2State*& current) { + Config* cfg = Config::Current(); + if(cfg->switcherEnabled) { + OBSWeakSource scene; + + if(previous->appState != current->appState) { + if(current->appState == APP_LOADING) { + // pre game loading screen + if(previous->appState == APP_MENU) { + scene = cfg->inGameScene; + } + } + + // entering game + if (current->appState == APP_INGAME && previous->appState != APP_INGAME) { + if (current->gameState == GAME_REPLAY) { + scene = cfg->replayScene; + } + else { + scene = cfg->inGameScene; + } + } + + // leaving game + if (current->appState == APP_MENU && previous->appState != APP_MENU) { + OBSWeakSource tmpScene; + tmpScene = cfg->menuScenes[current->menuState]; + if (!tmpScene || current->menuState == MENU_NONE) { + tmpScene = cfg->outGameScene; + } + scene = tmpScene; + } + } + + if (current->menuState != previous->menuState) { + if(current->appState == APP_MENU) { + OBSWeakSource tmpScene; + tmpScene = cfg->menuScenes[current->menuState]; + if (!tmpScene || current->menuState == MENU_NONE) { + tmpScene = cfg->outGameScene; + } + scene = tmpScene; + } + } + + if (current->gameState == GAME_OBS && current->appState == APP_INGAME) { + if(current->gameState == GAME_OBS) { + scene = cfg->obsScene; + } + } + + // scene change if necesary + if (scene) { + obs_source_t *source = obs_weak_source_get_source(scene); + obs_source_t *currentSource = obs_frontend_get_current_scene(); + + if (source && currentSource && source != currentSource) { + obs_frontend_set_current_scene(source); + } + obs_source_release(currentSource); + obs_source_release(source); + } + } +} \ No newline at end of file diff --git a/SceneSwitcher.h b/SceneSwitcher.h new file mode 100644 index 0000000..bc50cd4 --- /dev/null +++ b/SceneSwitcher.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Observer.h" +#include "SC2State.h" + +class SceneSwitcher : public Observer { + public: + explicit SceneSwitcher(SC2Data *sc2); + + public slots: + void notify(SC2State*& previous, SC2State*& current); +}; \ No newline at end of file diff --git a/ScoreTracker.cpp b/ScoreTracker.cpp new file mode 100644 index 0000000..949d97a --- /dev/null +++ b/ScoreTracker.cpp @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "SC2Data.h" +#include "Observer.h" +#include "SC2State.h" +#include "ScoreTracker.h" +#include "Constants.h" +#include "Config.h" +#include "APIState.h" +#include "obs-util.h" + +ScoreTracker* _instance = nullptr; + +ScoreTracker* ScoreTracker::Current() { + return _instance; +} + +ScoreTracker::~ScoreTracker() { + delete _instance; +} + +ScoreTracker::ScoreTracker(SC2Data *sc2): Observer(sc2){ + timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, this, [=]{ updateText(); }); + timer->start(1000); + _instance = this; +} + +void ScoreTracker::notify(SC2State*& previous, SC2State*& current) { + Config* cfg = Config::Current(); + if(cfg->scoresEnabled) { + if(previous->appState != current->appState && + current->appState == APP_MENU && previous->appState != APP_MENU) { + if(current->fullState.players.size() == 2 && !current->fullState.isReplay) { + bool iAmPlayerA = false; + bool iAmPlayerB = false; + for (size_t i = 0; i < current->fullState.players.size(); i++) { + for (size_t j= 0; j < cfg->usernames.size(); j++) { + bool found = current->fullState.players[i]->name == cfg->usernames[j]; + if(found && i==0) { + iAmPlayerA = true; + } + if(found && i==1) { + iAmPlayerB = true; + } + } + } + if (iAmPlayerA && iAmPlayerB) { + // sc2 only tells us the name and barcodes are a thing.common names could also cause stats to + // be recorded incorrectly.only way to get accurate info is asking the user to confirm + addConfirmMessage(current->fullState.players[0], current->fullState.players[1]); + } + else if (!iAmPlayerA && !iAmPlayerB) { + // we didnt know which player the user was so we have to ask + addConfirmMessage(current->fullState.players[0], current->fullState.players[1]); + } + else { + // normal result + if (iAmPlayerB) { + recordScore(current->fullState.players[0]->race, current->fullState.players[1]->result); + } + else { + recordScore(current->fullState.players[1]->race, current->fullState.players[0]->result); + } + } + } + } + } +} + +std::string ScoreTracker::getScoreString() { + // this probably needs refactoring + Config* cfg = Config::Current(); + std::string output = cfg->scoreString; + std::map > scoreMap; + + scoreMap[0]["search"] = "${tw}"; + scoreMap[0]["replace"] = to_string(scores["Terr"]["Victory"]); + scoreMap[1]["search"] = "${tl}"; + scoreMap[1]["replace"] = to_string(scores["Terr"]["Defeat"]); + + scoreMap[2]["search"] = "${zw}"; + scoreMap[2]["replace"] = to_string(scores["Zerg"]["Victory"]); + scoreMap[3]["search"] = "${zl}"; + scoreMap[3]["replace"] = to_string(scores["Zerg"]["Defeat"]); + + scoreMap[4]["search"] = "${pw}"; + scoreMap[4]["replace"] = to_string(scores["Prot"]["Victory"]); + scoreMap[5]["search"] = "${pl}"; + scoreMap[5]["replace"] = to_string(scores["Prot"]["Defeat"]); + + for (size_t i = 0; i < scoreMap.size(); i++) { + std::string search = scoreMap[i]["search"]; + std::string replace = scoreMap[i]["replace"]; + size_t pos = 0; + while ((pos = output.find(search, pos)) != std::string::npos) { + output.replace(pos, search.length(), replace); + pos += replace.length(); + } + } + + return output; +} + +void ScoreTracker::updateText() { + Config* cfg = Config::Current(); + obs_source_t *source; + try { + source = obs_get_source_by_name(cfg->textSourceName.c_str()); + } + catch (...) { + return; + } + if (source) { + obs_data_t *obj = obs_data_create(); + obs_data_set_string(obj, "text", getScoreString().c_str()); + obs_source_update(source, obj); + obs_data_release(obj); + } + obs_source_release(source); +} + +void ScoreTracker::recordScore(std::string race, std::string result) { + if (race == "random") { + addRandomConfirmMessage(result); + } + else { + scores[race][result] = scores[race][result] + 1; + updateText(); + } +} + +void ScoreTracker::handleButton(std::string race, std::string result, std::string name) { + Config* cfg = Config::Current(); + if (std::find(cfg->usernames.begin(), cfg->usernames.end(), name) == cfg->usernames.end()) { + cfg->usernames.push_back(name); + } + recordScore(race, result); +} + +void ScoreTracker::addRandomConfirmMessage(std::string result) { + Config* cfg = Config::Current(); + if(cfg->popupsEnabled) { + QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window(); + QMessageBox* msgBox = new QMessageBox(mainWindow); + msgBox->setText("Which race was your opponent playing?"); + + QAbstractButton* pButtonT = msgBox->addButton("Terran", QMessageBox::ActionRole); + QObject::connect(pButtonT, &QPushButton::released, this, [=]{ + handleButton("Terr", result, ""); + }); + + QAbstractButton* pButtonZ = msgBox->addButton("Zerg", QMessageBox::ActionRole); + QObject::connect(pButtonZ, &QPushButton::released, this, [=]{ + handleButton("Zerg", result, ""); + }); + + QAbstractButton* pButtonP = msgBox->addButton("Protoss", QMessageBox::ActionRole); + QObject::connect(pButtonP, &QPushButton::released, this, [=]{ + handleButton("Prot", result, ""); + }); + msgBox->setWindowModality(Qt::NonModal); + msgBox->setAttribute(Qt::WA_ShowWithoutActivating); + msgBox->setWindowFlags(Qt::Tool); + msgBox->open(); + } + else { + // find another way to handle them that doesnt steal focus + } +} + + +void ScoreTracker::addConfirmMessage(player* playerA, player* playerB) { + Config* cfg = Config::Current(); + if(cfg->popupsEnabled) { + QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window(); + QMessageBox* msgBox = new QMessageBox(mainWindow); + msgBox->setText("Which player were you?"); + + char playerAButton[50]; + sprintf(playerAButton, "%s: %s (%s)", playerA->race, playerA->name, playerA->result); + + QAbstractButton* pButtonT = msgBox->addButton(playerAButton, QMessageBox::ActionRole); + QObject::connect(pButtonT, &QPushButton::released, this, [=]{ + handleButton(playerB->race, playerA->result, playerA->name); + }); + + char playerBButton[50]; + sprintf(playerBButton, "%s: %s (%s)", playerB->race, playerB->name, playerB->result); + QAbstractButton* pButtonZ = msgBox->addButton(playerBButton, QMessageBox::ActionRole); + QObject::connect(pButtonZ, &QPushButton::released, this, [=]{ + handleButton(playerA->race, playerB->result, playerB->name); + }); + + msgBox->setWindowModality(Qt::NonModal); + msgBox->setAttribute(Qt::WA_ShowWithoutActivating); + msgBox->setWindowFlags(Qt::Tool); + msgBox->open(); + } + else { + // find another way to handle them that doesnt steal focus + } + +} + diff --git a/ScoreTracker.h b/ScoreTracker.h new file mode 100644 index 0000000..f672c3c --- /dev/null +++ b/ScoreTracker.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "Observer.h" +#include "SC2State.h" + +class ScoreTracker : public Observer { + private: + QTimer* timer; + public: + explicit ScoreTracker(SC2Data *sc2); + static ScoreTracker* Current(); + ~ScoreTracker(); + std::map < + std::string, + std::map + > scores; + std::string getScoreString(); + void recordScore(std::string race, std::string result); + void handleButton(std::string race, std::string result, std::string name); + void addRandomConfirmMessage(std::string result); + void addConfirmMessage(player* playerA, player* playerB); + + public slots: + void notify(SC2State*& previous, SC2State*& current); + void updateText(); +}; \ No newline at end of file diff --git a/Webhook.cpp b/Webhook.cpp new file mode 100644 index 0000000..bd62489 --- /dev/null +++ b/Webhook.cpp @@ -0,0 +1,130 @@ +#include + +#include "SC2Data.h" +#include "Observer.h" +#include "SC2State.h" +#include "Webhook.h" +#include "Constants.h" +#include "Config.h" +#include "obs-util.h" +#include "APIState.h" +#include "ScoreTracker.h" + +Webhook::Webhook(SC2Data *sc2): Observer(sc2){ } + +void Webhook::notify(SC2State*& previous, SC2State*& current) { + Config* cfg = Config::Current(); + if(cfg->webhookEnabled) { + std::string event = ""; + + if(previous->appState != current->appState && + current->appState != APP_INGAME && previous->appState == APP_INGAME) { + if(current->fullState.players.size() == 2 && !current->fullState.isReplay) { + event = "exit"; + } + } + + if(previous->appState != current->appState && + current->appState == APP_INGAME && previous->appState != APP_INGAME) { + event = "enter"; + } + + if(event != "") { + sendRequest(current, event); + } + } + } + + void Webhook::sendRequest(SC2State*& game, std::string event) { + if(!game->fullState.isReplay) { + CURL *curl; + CURLcode res; + + Config* cfg = Config::Current(); + + // todo: make this a multi curl + for (string &url : cfg->webhookURLList) { + curl = curl_easy_init(); + if (curl) { + std::string qdelim = "?"; + std::size_t found = url.find(qdelim); + if (found!=std::string::npos) { + qdelim = "&"; + } + + std::string resp = getJSONStringFromSC2State(game, event); + std::string c_url = url + qdelim + "json=" + curl_easy_escape(curl, resp.c_str(), 0); + curl_easy_setopt(curl, CURLOPT_URL, c_url.c_str()); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 150); + res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + if (res != CURLE_OK) { + // pass + } + } + } + } + } + + std::string Webhook::getQueryStringFromSC2State(SC2State*& game, std::string event, CURL* curl) { + Config* cfg = Config::Current(); + std::string resp = ""; + for(size_t i=0; ifullState.players.size(); i++) { + std::string index = std::to_string(i); + resp = resp + "&players[" + index + "][name]=" + curl_easy_escape(curl, game->fullState.players[i]->name, 0); + resp = resp + "&players[" + index + "][type]=" + curl_easy_escape(curl, game->fullState.players[i]->type, 0); + resp = resp + "&players[" + index + "][race]=" + curl_easy_escape(curl, game->fullState.players[i]->race, 0); + resp = resp + "&players[" + index + "][result]=" + curl_easy_escape(curl, game->fullState.players[i]->result, 0); + if (std::find(cfg->usernames.begin(), cfg->usernames.end(), game->fullState.players[i]->name) != cfg->usernames.end()) { + resp = resp + "&players[" + index + "][isme]=true"; + } + else { + resp = resp + "&players[" + index + "][isme]=false"; + } + } + + std::string dp = std::to_string(game->fullState.displayTime); + resp = resp + "&displayTime=" + dp; + resp = resp + "&event=" + event; + return resp; + } + +std::string Webhook::getJSONStringFromSC2State(SC2State*& game, std::string event) { + // i know this is dirty but im just testing it as the query string option + // doesnt work with node. ill update it... + Config* cfg = Config::Current(); + std::string resp = ""; + resp = resp + "{\"players\": ["; + for(size_t i=0; ifullState.players.size(); i++) { + resp = resp + "{"; + resp = resp + "\"name\": \"" + game->fullState.players[i]->name + "\","; + resp = resp + "\"type\": \"" + game->fullState.players[i]->type + "\","; + resp = resp + "\"race\": \"" + game->fullState.players[i]->race + "\","; + resp = resp + "\"result\": \"" + game->fullState.players[i]->result + "\","; + + if (std::find(cfg->usernames.begin(), cfg->usernames.end(), game->fullState.players[i]->name) != cfg->usernames.end()) { + resp = resp + "\"isme\": true"; + } + else { + resp = resp + "\"isme\": false"; + } + resp = resp + "}"; + if(i + 1 != game->fullState.players.size()) { + resp = resp + ","; + } + } + resp = resp + "],"; + std::string dp = std::to_string(game->fullState.displayTime); + resp = resp + "\"displayTime\": \"" + dp + "\","; + resp = resp + "\"event\": \"" + event + "\","; + + ScoreTracker* st = ScoreTracker::Current(); + resp = resp + "\"scores\": { "; + resp = resp + "\"Terr\": {\"Victory\": " + to_string(st->scores["Terr"]["Victory"]) + ", \"Defeat\": " + to_string(st->scores["Terr"]["Defeat"]) + " },"; + resp = resp + "\"Prot\": {\"Victory\": " + to_string(st->scores["Prot"]["Victory"]) + ", \"Defeat\": " + to_string(st->scores["Prot"]["Defeat"]) + " },"; + resp = resp + "\"Zerg\": {\"Victory\": " + to_string(st->scores["Zerg"]["Victory"]) + ", \"Defeat\": " + to_string(st->scores["Zerg"]["Defeat"]) + " }"; + resp = resp + "}"; + + resp = resp + "}"; + return resp; + } \ No newline at end of file diff --git a/Webhook.h b/Webhook.h new file mode 100644 index 0000000..b0a4bd8 --- /dev/null +++ b/Webhook.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Observer.h" +#include "SC2State.h" +#include + +class Webhook : public Observer { + private: + void sendRequest(SC2State*& game, std::string event); + std::string getQueryStringFromSC2State(SC2State*& game, std::string event, CURL* curl); + std::string getJSONStringFromSC2State(SC2State*& game, std::string event); + + public: + explicit Webhook(SC2Data *sc2); + + public slots: + void notify(SC2State*& previous, SC2State*& current); + +}; \ No newline at end of file diff --git a/auto-scene-switcher.cpp b/auto-scene-switcher.cpp deleted file mode 100644 index 303fe8e..0000000 --- a/auto-scene-switcher.cpp +++ /dev/null @@ -1,448 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include "auto-scene-switcher.hpp" -#include "tool-helpers.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -struct SwitcherData { - thread th; - condition_variable cv; - mutex m; - bool stop = false; - - void Thread(); - void Start(); - void Stop(); - void api_callback(); - - OBSWeakSource inGameScene; - OBSWeakSource outGameScene; - OBSWeakSource replayScene; - string ipAddr; - - int state = 0; - int apiState = 0; - bool apiInReplay = false; - - int STATE_INGAME = 1; - int STATE_REPLAY = 2; - int STATE_MENUS = 3; - int STATE_LOADING = 4; - - int interval = 1500; - - inline SwitcherData() { - curl_global_init(CURL_GLOBAL_DEFAULT); - } - - inline ~SwitcherData() - { - Stop(); - curl_global_cleanup(); - } -}; -static SwitcherData *switcher = nullptr; - -void SwitcherData::Start() -{ - if (!switcher->th.joinable()) - switcher->th = thread([]() {switcher->Thread(); }); -} - -void SwitcherData::Stop() -{ - if (th.joinable()) { - { - lock_guard lock(m); - stop = true; - } - cv.notify_one(); - th.join(); - } -} - -static inline bool WeakSourceValid(obs_weak_source_t *ws) -{ - obs_source_t *source = obs_weak_source_get_source(ws); - if (source) - obs_source_release(source); - return !!source; -} - -SceneSwitcher::SceneSwitcher(QWidget *parent) - : QDialog(parent), - ui(new Ui_SceneSwitcher) -{ - ui->setupUi(this); - lock_guard lock(switcher->m); - - ui->inGameScene->addItem(""); - ui->outGameScene->addItem(""); - ui->replayScene->addItem(""); - BPtr scenes = obs_frontend_get_scene_names(); - char **temp = scenes; - while (*temp) { - const char *name = *temp; - ui->inGameScene->addItem(name); - ui->outGameScene->addItem(name); - ui->replayScene->addItem(name); - temp++; - } - - ui->inGameScene->setCurrentText(GetWeakSourceName(switcher->inGameScene).c_str()); - ui->outGameScene->setCurrentText(GetWeakSourceName(switcher->outGameScene).c_str()); - ui->replayScene->setCurrentText(GetWeakSourceName(switcher->replayScene).c_str()); - ui->ipAddr->setText(QString::fromStdString(switcher->ipAddr)); - - if (switcher->th.joinable()) { - ui->toggleStartButton->setText(obs_module_text("Stop")); - } - else { - ui->toggleStartButton->setText(obs_module_text("Start")); - } - - loading = false; -} - -static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) -{ - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -void SwitcherData::api_callback() { - CURL *curl; - CURLcode res; - std::string UIResponse; - - curl = curl_easy_init(); - if (curl) { - string reqURL = switcher->ipAddr; - if (reqURL == "") { - reqURL = "localhost"; - } - reqURL = "http://" + reqURL + ":6119/ui"; - - curl_easy_setopt(curl, CURLOPT_URL, reqURL.c_str()); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 500); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &UIResponse); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - return; - } - } - curl_easy_cleanup(curl); - - std::string gameResponse; - - curl = curl_easy_init(); - if (curl) { - string reqURL = switcher->ipAddr; - if (reqURL == "") { - reqURL = "localhost"; - } - reqURL = "http://" + reqURL + ":6119/game"; - - curl_easy_setopt(curl, CURLOPT_URL, reqURL.c_str()); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 500); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &gameResponse); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - return; - } - } - curl_easy_cleanup(curl); - - lock_guard lock(switcher->m); - - json_error_t error; - json_t* root = json_loads(UIResponse.c_str(), 0, &error); - if (!root) { - return; - } - json_t* screens = json_object_get(root, "activeScreens"); - if (json_array_size(screens) > 1) { - /* - this is where you could extend this to other screens - you have to iterate over them all, they arent in a consistent order - - interesting ones: - - ScreenMultiplayer/ScreenMultiplayer - ScreenMatchmaking/ScreenMatchmaking - ScreenCustomGames/ScreenCustomGames - ScreenBattleLobby/ScreenBattleLobby - ScreenTournament/ScreenTournament - - ScreenReplay/ScreenReplay - ScreenCoopCampaign/ScreenCoopCampaign - ScreenCollection/ScreenCollection - - ScreenSingle/ScreenSingle // (campaign) - */ - - // in menus - switcher->apiState = STATE_MENUS; - } - else if (json_array_size(screens) == 0 ) { - // in game - switcher->apiState = STATE_INGAME; - } - else { - // we are in the loading screen - switcher->apiState = STATE_LOADING; - } - - root = json_loads(gameResponse.c_str(), 0, &error); - if (!root) { - return; - } - json_t* isReplay = json_object_get(root, "isReplay"); - switcher->apiInReplay = (isReplay == json_true()); - //if (isReplay == json_true()) { - // switcher->apiInReplay = true; - //} - //else { - // switcher->apiInReplay = false; - //} -} - -void SwitcherData::Thread() { - chrono::duration duration = - chrono::milliseconds(interval); - - for (;;) { - thread t(&SwitcherData::api_callback, this); - - unique_lock lock(m); - cv.wait_for(lock, duration); - - t.join(); - if (switcher->stop) { - switcher->stop = false; - break; - } - duration = chrono::milliseconds(interval); - - OBSWeakSource scene; - if (switcher->apiState != switcher->state) { - if (switcher->apiState == STATE_LOADING) { - if (switcher->state == STATE_MENUS) { - // enter game event, show in game scene for loading screen w/ player info etc - scene = switcher->inGameScene; - } - else if (switcher->state == STATE_INGAME ) { - // exit game event - scene = switcher->outGameScene; - } - } - if (switcher->apiState == STATE_INGAME && switcher->state != STATE_INGAME) { - if (switcher->apiInReplay) { - scene = switcher->replayScene; - } - else { - scene = switcher->inGameScene; - } - } - if (switcher->apiState == STATE_MENUS && switcher->state != STATE_MENUS) { - scene = switcher->outGameScene; - } - // update the state - switcher->state = switcher->apiState; - } - - if (scene) { - obs_source_t *source = obs_weak_source_get_source(scene); - obs_source_t *currentSource = obs_frontend_get_current_scene(); - - if (source && source != currentSource) - obs_frontend_set_current_scene(source); - - obs_source_release(currentSource); - obs_source_release(source); - - } - } -} - -void SceneSwitcher::closeEvent(QCloseEvent*) -{ - obs_frontend_save(); -} - -void SceneSwitcher::on_inGameScene_currentTextChanged(const QString& text) -{ - if (!loading) { - lock_guard lock(switcher->m); - obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); - obs_weak_source_t* ws = obs_source_get_weak_source(scene); - - switcher->inGameScene = ws; - - obs_weak_source_release(ws); - obs_source_release(scene); - } -} - -void SceneSwitcher::on_outGameScene_currentTextChanged(const QString& text) -{ - if (!loading) { - lock_guard lock(switcher->m); - obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); - obs_weak_source_t* ws = obs_source_get_weak_source(scene); - - switcher->outGameScene = ws; - - obs_weak_source_release(ws); - obs_source_release(scene); - } -} - -void SceneSwitcher::on_replayScene_currentTextChanged(const QString& text) -{ - if (!loading) { - lock_guard lock(switcher->m); - obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); - obs_weak_source_t* ws = obs_source_get_weak_source(scene); - - switcher->replayScene = ws; - - obs_weak_source_release(ws); - obs_source_release(scene); - } -} - -void SceneSwitcher::on_ipAddr_textChanged(const QString& text) -{ - if (!loading) { - lock_guard lock(switcher->m); - switcher->ipAddr = (string)text.toUtf8().constData(); - } -} - -void SceneSwitcher::on_toggleStartButton_clicked() -{ - if (switcher->th.joinable()) { - switcher->Stop(); - ui->toggleStartButton->setText(obs_module_text("Start")); - } - else { - switcher->Start(); - ui->toggleStartButton->setText(obs_module_text("Stop")); - } -} - -void SceneSwitcher::on_exitButton_clicked() -{ - close(); -} - -static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *) -{ - if (saving) { - lock_guard lock(switcher->m); - obs_data_t *obj = obs_data_create(); - - string inGameSceneName = GetWeakSourceName(switcher->inGameScene); - obs_data_set_string(obj, "in_game_scene", inGameSceneName.c_str()); - - string outGameSceneName = GetWeakSourceName(switcher->outGameScene); - obs_data_set_string(obj, "out_game_scene", outGameSceneName.c_str()); - - string replaySceneName = GetWeakSourceName(switcher->replayScene); - obs_data_set_string(obj, "replay_scene", replaySceneName.c_str()); - - obs_data_set_bool(obj, "is_running", switcher->th.joinable()); - obs_data_set_string(obj, "ip_addr", switcher->ipAddr.c_str()); - - obs_data_set_obj(save_data, "sc2switcher", obj); - obs_data_release(obj); - } - else { - switcher->m.lock(); - obs_data_t *obj = obs_data_get_obj(save_data, "sc2switcher"); - - if (!obj) - obj = obs_data_create(); - - string inGameScene = - obs_data_get_string(obj, "in_game_scene"); - switcher->inGameScene = GetWeakSourceByName(inGameScene.c_str()); - - string outGameScene = - obs_data_get_string(obj, "out_game_scene"); - switcher->outGameScene = GetWeakSourceByName(outGameScene.c_str()); - - string replayScene = - obs_data_get_string(obj, "replay_scene"); - switcher->replayScene = GetWeakSourceByName(replayScene.c_str()); - - string ipAddr = obs_data_get_string(obj, "ip_addr"); - switcher->ipAddr = ipAddr; - - switcher->m.unlock(); - - if (obs_data_get_bool(obj, "is_running")) { - switcher->Start(); - } - else { - switcher->Stop(); - } - - obs_data_release(obj); - } -} - -extern "C" void FreeSceneSwitcher() -{ - delete switcher; - switcher = nullptr; -} - -static void OBSEvent(enum obs_frontend_event event, void *) -{ - if (event == OBS_FRONTEND_EVENT_EXIT) - FreeSceneSwitcher(); -} - -extern "C" void InitSceneSwitcher() -{ - QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction( - "SC2 Scene Switcher"); - - switcher = new SwitcherData; - auto cb = []() - { - obs_frontend_push_ui_translation(obs_module_get_string); - - QMainWindow *window = - (QMainWindow*)obs_frontend_get_main_window(); - - SceneSwitcher ss(window); - ss.exec(); - - obs_frontend_pop_ui_translation(); - }; - - obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr); - obs_frontend_add_event_callback(OBSEvent, nullptr); - action->connect(action, &QAction::triggered, cb); -} diff --git a/auto-scene-switcher.hpp b/auto-scene-switcher.hpp deleted file mode 100644 index 8c3b3d9..0000000 --- a/auto-scene-switcher.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "ui_auto-scene-switcher.h" -struct obs_weak_source; -typedef struct obs_weak_source obs_weak_source_t; - -class QCloseEvent; - -class SceneSwitcher : public QDialog { - Q_OBJECT - -public: - bool loading = true; - -public: - std::unique_ptr ui; - SceneSwitcher(QWidget *parent); - void closeEvent(QCloseEvent *event) override; - - public slots: - void on_inGameScene_currentTextChanged(const QString &name); - void on_outGameScene_currentTextChanged(const QString &name); - void on_replayScene_currentTextChanged(const QString& text); - void on_ipAddr_textChanged(const QString &name); - void on_toggleStartButton_clicked(); - void on_exitButton_clicked(); -}; diff --git a/forms/SettingsDialog.cpp b/forms/SettingsDialog.cpp new file mode 100644 index 0000000..740ff59 --- /dev/null +++ b/forms/SettingsDialog.cpp @@ -0,0 +1,592 @@ +#include +#include + +#include "ScoreTracker.h" +#include "SettingsDialog.h" +#include "Config.h" +#include "Constants.h" +#include "obs-util.h" + +using namespace std; + +Config* config = Config::Current(); + +SettingsDialog::SettingsDialog(QWidget* parent) : + QDialog(parent, Qt::Dialog), + ui(new Ui::SettingsDialog) +{ + isLoading = true; + ui->setupUi(this); + + // fill them boxes + ui->inGameScene->addItem(""); + ui->outGameScene->addItem(""); + ui->replayScene->addItem(""); + + ui->obsScene->addItem(""); + ui->replaysScene->addItem(""); + ui->campaignScene->addItem(""); + ui->homeScene->addItem(""); + ui->collectionScene->addItem(""); + ui->coopScene->addItem(""); + ui->customScene->addItem(""); + ui->lobbyScene->addItem(""); + ui->scoreScreenScene->addItem(""); + ui->profileScene->addItem(""); + ui->versusScene->addItem(""); + + BPtr scenes = obs_frontend_get_scene_names(); + char **temp = scenes; + while (*temp) { + const char *name = *temp; + ui->inGameScene->addItem(name); + ui->outGameScene->addItem(name); + ui->replayScene->addItem(name); + ui->obsScene->addItem(name); + ui->replaysScene->addItem(name); + ui->campaignScene->addItem(name); + ui->homeScene->addItem(name); + ui->collectionScene->addItem(name); + ui->coopScene->addItem(name); + ui->customScene->addItem(name); + ui->lobbyScene->addItem(name); + ui->scoreScreenScene->addItem(name); + ui->profileScene->addItem(name); + ui->versusScene->addItem(name); + + temp++; + } + + ui->inGameScene->setCurrentText(GetWeakSourceName(config->inGameScene).c_str()); + ui->outGameScene->setCurrentText(GetWeakSourceName(config->outGameScene).c_str()); + ui->replayScene->setCurrentText(GetWeakSourceName(config->replayScene).c_str()); + + // there has GOT to be a better way to do this + ui->obsScene->setCurrentText(GetWeakSourceName(config->obsScene).c_str()); + ui->replaysScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_REPLAYS]).c_str()); + ui->campaignScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_CAMPAIGN]).c_str()); + ui->homeScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_HOME]).c_str()); + ui->collectionScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_COLLECTION]).c_str()); + ui->coopScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_COOP]).c_str()); + ui->customScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_CUSTOM]).c_str()); + ui->lobbyScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_LOBBY]).c_str()); + ui->scoreScreenScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_SCORESCREEN]).c_str()); + ui->profileScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_PROFILE]).c_str()); + ui->versusScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_VERSUS]).c_str()); + + for (size_t i = 0; i < config->usernames.size(); i++) { + ui->userNames->addItem(config->usernames[i].c_str()); + } + + for (size_t i = 0; i < config->recentUsernames.size(); i++) { + ui->recentUsernames->addItem(config->recentUsernames[i].c_str()); + } + + ui->ipAddr->setText(QString::fromStdString(config->ipAddr)); + + config->checkForUpdates(); + if(config->updateURL != "") { + ui->downloadURL->setText(QString::fromStdString("updateURL + "\">Update Available. Click here to download.")); + ui->downloadURL->setTextFormat(Qt::RichText); + ui->downloadURL->setTextInteractionFlags(Qt::TextBrowserInteraction); + ui->downloadURL->setOpenExternalLinks(true); + ui->patchNotesLabel->setText(config->updateDescription.c_str()); + } + + if (config->isRunning) { + ui->toggleStartButton->setText("Stop"); + } + else { + ui->toggleStartButton->setText("Start"); + } + + ui->textTemplate->setText(QString::fromStdString(config->scoreString)); + ui->textSourceName->setText(QString::fromStdString(config->textSourceName)); + + if(config->switcherEnabled) { + ui->switcherEnabled->setChecked(true); + } + if(config->scoresEnabled) { + ui->scoresEnabled->setChecked(true); + } + if(config->popupsEnabled) { + ui->popupsEnabled->setChecked(true); + } + + if(config->webhookEnabled) { + ui->webhookEnabled->setChecked(true); + } + + for (size_t i = 0; i < config->webhookURLList.size(); i++) { + ui->webhookURLList->addItem(config->webhookURLList[i].c_str()); + } + + ScoreTracker* st = ScoreTracker::Current(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + + + isLoading = false; +} + +SettingsDialog::~SettingsDialog() { + delete ui; +} + +void SettingsDialog::closeEvent(QCloseEvent*) { + obs_frontend_save(); +} + + +void SettingsDialog::on_inGameScene_currentTextChanged(const QString& text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->inGameScene = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_outGameScene_currentTextChanged(const QString& text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->outGameScene = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_replayScene_currentTextChanged(const QString& text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->replayScene = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_ipAddr_textChanged(const QString& text) { + if(!isLoading) { + config->ipAddr = (string)text.toUtf8().constData(); + } +} + +void SettingsDialog::on_toggleStartButton_clicked() +{ + if (config->isRunning) { + config->isRunning = false; + ui->toggleStartButton->setText("Start"); + } + else { + config->isRunning = true; + ui->toggleStartButton->setText("Stop"); + } +} + +void SettingsDialog::on_obsScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->obsScene = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_replaysScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_REPLAYS] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_campaignScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_CAMPAIGN] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_homeScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_HOME] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_collectionScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_COLLECTION] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } + +} + +void SettingsDialog::on_coopScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_COOP] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } + +} + +void SettingsDialog::on_customScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_CUSTOM] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_lobbyScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_LOBBY] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_scoreScreenScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_SCORESCREEN] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_profileScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_PROFILE] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_versusScene_currentTextChanged(const QString &text) { + if(!isLoading) { + obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); + obs_weak_source_t* ws = obs_source_get_weak_source(scene); + + config->menuScenes[MENU_VERSUS] = ws; + + obs_weak_source_release(ws); + obs_source_release(scene); + } +} + +void SettingsDialog::on_addUsernameButton_clicked() { + if(!isLoading) { + // add the username to our usernames + string username = ui->usernameLine->text().toUtf8().constData(); + config->usernames.push_back(username); + + // add it to the ui list + ui->userNames->addItem(ui->usernameLine->text()); + + // clear the username input + ui->usernameLine->setText(""); + } +} + +void SettingsDialog::on_removeUsernameButton_clicked() { + if(!isLoading) { + // remove the list item from our usernames + config->usernames.erase(std::remove(config->usernames.begin(), config->usernames.end(), ui->usernameLine->text().toUtf8().constData()), config->usernames.end()); + + // remove the list item from the ui list + // have to iterate backwards because the size of the list changes as items are removed + // also store the name first as the value of usernameLine changes as items are removed and new items selected + string searchName = ui->usernameLine->text().toUtf8().constData(); + for (int i = ui->userNames->count()-1; i >= 0; i--) { + QListWidgetItem *item = ui->userNames->item(i); + if (item->text() == QString::fromStdString(searchName)) { + int row = ui->userNames->row(item); + delete ui->userNames->takeItem(row); + } + } + + // clear the username box if its empty + // otherwise box will hold selected item + if (ui->userNames->count() == 0) { + ui->usernameLine->setText(""); + } + } +} + +void SettingsDialog::on_userNames_itemSelectionChanged() { + if(!isLoading) { + if (ui->userNames->currentItem() != NULL && ui->userNames->currentItem()->text() != NULL) { + ui->usernameLine->setText(ui->userNames->currentItem()->text()); + } + } +} + +void SettingsDialog::on_recentUsernames_itemSelectionChanged() { + if(!isLoading) { + if (ui->recentUsernames->currentItem() != NULL && ui->recentUsernames->currentItem()->text() != NULL) { + ui->usernameLine->setText(ui->recentUsernames->currentItem()->text()); + } + } +} + +void SettingsDialog::on_textSourceName_textChanged(const QString& text) { + if(!isLoading) { + if (config->textSourceName != (string)text.toUtf8().constData()) { + config->textSourceName = (string)text.toUtf8().constData(); + } + } +} + +void SettingsDialog::on_textTemplate_textChanged() { + if(!isLoading) { + if (config->scoreString != (string)ui->textTemplate->toPlainText().toUtf8().constData()) { + config->scoreString = (string)ui->textTemplate->toPlainText().toUtf8().constData(); + } + } +} + +void SettingsDialog::on_switcherEnabled_stateChanged(int state) { + if(state == Qt::Checked) { + config->switcherEnabled = true; + } + else { + config->switcherEnabled = false; + } +} + + +void SettingsDialog::on_scoresEnabled_stateChanged(int state) { + if(state == Qt::Checked) { + config->scoresEnabled = true; + } + else { + config->scoresEnabled = false; + } +} + +void SettingsDialog::on_popupsEnabled_stateChanged(int state) { + if(state == Qt::Checked) { + config->popupsEnabled = true; + } + else { + config->popupsEnabled = false; + } +} + + +void SettingsDialog::on_webhookEnabled_stateChanged(int state){ + if(state == Qt::Checked) { + config->webhookEnabled = true; + } + else { + config->webhookEnabled = false; + } +} + +void SettingsDialog::on_tWinPlus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading && st) { + st->scores["Terr"]["Victory"] = st->scores["Terr"]["Victory"] + 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } +} + +void SettingsDialog::on_tWinMinus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if (!isLoading) { + if (st->scores["Terr"]["Victory"] > 0) { + st->scores["Terr"]["Victory"] = st->scores["Terr"]["Victory"] - 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } + } +} + +void SettingsDialog::on_tLossPlus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading) { + st->scores["Terr"]["Defeat"] = st->scores["Terr"]["Defeat"] + 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } +} + +void SettingsDialog::on_tLossMinus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if (!isLoading) { + if (st->scores["Terr"]["Defeat"] > 0) { + st->scores["Terr"]["Defeat"] = st->scores["Terr"]["Defeat"] - 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } + } +} + +void SettingsDialog::on_zWinPlus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading) { + st->scores["Zerg"]["Victory"] = st->scores["Zerg"]["Victory"] + 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } +} + +void SettingsDialog::on_zWinMinus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if (!isLoading) { + if (st->scores["Zerg"]["Victory"] > 0) { + st->scores["Zerg"]["Victory"] = st->scores["Zerg"]["Victory"] - 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } + } +} + +void SettingsDialog::on_zLossPlus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading) { + st->scores["Zerg"]["Defeat"] = st->scores["Zerg"]["Defeat"] + 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } +} + +void SettingsDialog::on_zLossMinus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if (!isLoading) { + if (st->scores["Zerg"]["Defeat"] > 0) { + st->scores["Zerg"]["Defeat"] = st->scores["Zerg"]["Defeat"] - 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } + } +} + +void SettingsDialog::on_pWinPlus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading) { + st->scores["Prot"]["Victory"] = st->scores["Prot"]["Victory"] + 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } +} + +void SettingsDialog::on_pWinMinus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if (!isLoading) { + if (st->scores["Prot"]["Victory"] > 0) { + st->scores["Prot"]["Victory"] = st->scores["Prot"]["Victory"] - 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } + } +} + +void SettingsDialog::on_pLossPlus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading) { + st->scores["Prot"]["Defeat"] = st->scores["Prot"]["Defeat"] + 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } +} + +void SettingsDialog::on_pLossMinus_clicked() { + ScoreTracker* st = ScoreTracker::Current(); + if(!isLoading) { + if (st->scores["Prot"]["Defeat"] > 0) { + st->scores["Prot"]["Defeat"] = st->scores["Prot"]["Defeat"] - 1; + st->updateText(); + ui->scoresLabel->setText(st->getScoreString().c_str()); + } + } +} + + +void SettingsDialog::on_addURLButton_clicked() { + if(!isLoading) { + string username = ui->webhookEnterGame->text().toUtf8().constData(); + config->webhookURLList.push_back(username); + ui->webhookURLList->addItem(ui->webhookEnterGame->text()); + ui->webhookEnterGame->setText(""); + } +} + +void SettingsDialog::on_removeURLButton_clicked() { + if(!isLoading) { + config->webhookURLList.erase(std::remove(config->webhookURLList.begin(), config->webhookURLList.end(), ui->webhookEnterGame->text().toUtf8().constData()), config->webhookURLList.end()); + + string searchName = ui->webhookEnterGame->text().toUtf8().constData(); + for (int i = ui->webhookURLList->count()-1; i >= 0; i--) { + QListWidgetItem *item = ui->webhookURLList->item(i); + if (item->text() == QString::fromStdString(searchName)) { + int row = ui->webhookURLList->row(item); + delete ui->webhookURLList->takeItem(row); + } + } + + if (ui->webhookURLList->count() == 0) { + ui->webhookEnterGame->setText(""); + } + } +} + +void SettingsDialog::on_webhookURLList_itemSelectionChanged() { + if(!isLoading) { + if (ui->webhookURLList->currentItem() != NULL && ui->webhookURLList->currentItem()->text() != NULL) { + ui->webhookEnterGame->setText(ui->webhookURLList->currentItem()->text()); + } + } +} \ No newline at end of file diff --git a/forms/SettingsDialog.h b/forms/SettingsDialog.h new file mode 100644 index 0000000..6ec3f82 --- /dev/null +++ b/forms/SettingsDialog.h @@ -0,0 +1,66 @@ +#pragma once +#include +#include "ui_SettingsDialog.h" + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget* parent = 0); + ~SettingsDialog(); + void closeEvent(QCloseEvent *event) override; + + public slots: + void on_toggleStartButton_clicked(); + void on_ipAddr_textChanged(const QString &name); + void on_addUsernameButton_clicked(); + void on_removeUsernameButton_clicked(); + + void on_userNames_itemSelectionChanged(); + void on_recentUsernames_itemSelectionChanged(); + void on_textSourceName_textChanged(const QString& text); + void on_textTemplate_textChanged(); + + void on_inGameScene_currentTextChanged(const QString &name); + void on_outGameScene_currentTextChanged(const QString &name); + void on_replayScene_currentTextChanged(const QString& text); + void on_obsScene_currentTextChanged(const QString &name); + void on_replaysScene_currentTextChanged(const QString &name); + void on_campaignScene_currentTextChanged(const QString &name); + void on_homeScene_currentTextChanged(const QString &name); + void on_collectionScene_currentTextChanged(const QString &name); + void on_coopScene_currentTextChanged(const QString &name); + void on_customScene_currentTextChanged(const QString &name); + void on_lobbyScene_currentTextChanged(const QString &name); + void on_scoreScreenScene_currentTextChanged(const QString &name); + void on_profileScene_currentTextChanged(const QString &name); + void on_versusScene_currentTextChanged(const QString &name); + void on_switcherEnabled_stateChanged(int state); + void on_scoresEnabled_stateChanged(int state); + void on_popupsEnabled_stateChanged(int state); + + void on_webhookEnabled_stateChanged(int state); + + void on_tWinPlus_clicked(); + void on_tWinMinus_clicked(); + void on_tLossPlus_clicked(); + void on_tLossMinus_clicked(); + void on_zWinPlus_clicked(); + void on_zWinMinus_clicked(); + void on_zLossPlus_clicked(); + void on_zLossMinus_clicked(); + void on_pWinPlus_clicked(); + void on_pWinMinus_clicked(); + void on_pLossPlus_clicked(); + void on_pLossMinus_clicked(); + + void on_addURLButton_clicked(); + void on_removeURLButton_clicked(); + void on_webhookURLList_itemSelectionChanged(); + + +private: + Ui::SettingsDialog* ui; + bool isLoading = false; +}; \ No newline at end of file diff --git a/forms/SettingsDialog.ui b/forms/SettingsDialog.ui new file mode 100755 index 0000000..fc4e1f3 --- /dev/null +++ b/forms/SettingsDialog.ui @@ -0,0 +1,938 @@ + + + SettingsDialog + + + + 0 + 0 + 590 + 419 + + + + SC2Switcher + + + + + 0 + 0 + 591 + 421 + + + + 0 + + + + Settings + + + + + 40 + 40 + 511 + 331 + + + + + + + + + + + + + + + + + 75 + true + + + + If SC2 is running on a different PC: (optional) + + + true + + + + + + + + + SC2 PC IP Address: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + <html><head/><body><p>On your SC2 PC, open the Battle.net launcher, select SC2, click Options, Game Settings, and under SC2, check 'Additional Command Line Arguments', and paste this into the textbox:</p></body></html> + + + true + + + + + + + -clientapi 6119 + + + true + + + + + + + + + + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + Usernames + + + + + 40 + 150 + 501 + 171 + + + + + + + + + Usernames: + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + + + + + + + + + Recent Usernames: + + + + + + + + + + + + + + 140 + 20 + 301 + 100 + + + + + + + Add Username: + + + + + + + + + + + + Add + + + + + + + Remove + + + + + + + + + + + Scene Switcher + + + + + 50 + 20 + 471 + 301 + + + + + + + Scene Switcher Enabled + + + + + + + + + + + In Game: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + Out of Game: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + Qt::RightToLeft + + + Replay: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + Qt::RightToLeft + + + <html><head/><body><p align="right">Observing:</p></body></html> + + + + + + + + + + + + + + Extra Scenes + + + + + 20 + 10 + 251 + 351 + + + + + + + Score Screen + + + + + + + + + + Home + + + + + + + + + + Co-op Menu + + + + + + + + + + Custom Games Menu + + + + + + + + + + Replays Menu + + + + + + + + + + + + 310 + 10 + 241 + 351 + + + + + + + Profile + + + + + + + + + + Lobby + + + + + + + + + + Campaign Menu + + + + + + + + + + Versus Menu + + + + + + + + + + Collection Menu + + + + + + + + + + + + Score Tracker + + + + + 270 + 20 + 261 + 151 + + + + + + + Text Template: + + + + + + + + + + + + <html><head/><body><p><span style=" font-weight:600;">${tw}</span> Terran Win<br/><span style=" font-weight:600;">${tl}</span> Terran Loss<br/><span style=" font-weight:600;">${zw}</span> Zerg Win<br/><span style=" font-weight:600;">${zl}</span> Zerg Loss<br/><span style=" font-weight:600;">${pw}</span> Protoss Win<br/><span style=" font-weight:600;">${pl}</span> Protoss Loss</p></body></html> + + + + + + + + + + + 50 + 20 + 191 + 116 + + + + + + + Score Tracker Enabled + + + + + + + Popups Enabled + + + + + + + + + Name of text source: + + + + + + + + + + + + + + 380 + 200 + 141 + 151 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + 20 + 200 + 321 + 175 + + + + + 5 + + + QLayout::SetFixedSize + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 35 + + + + + + + + Terran + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Zerg + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Protoss + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + 15 + + + + + <html><head/><body><p align="center">Win</p></body></html> + + + + + + + 5 + + + + + + 55 + 55 + + + + + + + + + + + + + 55 + 55 + + + + - + + + + + + + + + 5 + + + + + + 55 + 55 + + + + + + + + + + + + + 55 + 55 + + + + - + + + + + + + + + 5 + + + + + + 55 + 55 + + + + + + + + + + + + + 55 + 55 + + + + - + + + + + + + + + + + 15 + + + + + <html><head/><body><p align="center">Loss</p></body></html> + + + + + + + 5 + + + + + + 55 + 55 + + + + + + + + + + + + + 55 + 55 + + + + - + + + + + + + + + 5 + + + + + + 55 + 55 + + + + + + + + + + + + + 55 + 55 + + + + - + + + + + + + + + 5 + + + + + + 55 + 55 + + + + + + + + + + + + + 55 + 55 + + + + - + + + + + + + + + + + + + Game Webhook + + + + + 70 + 30 + 191 + 22 + + + + Webhook Enabled + + + + + + 70 + 60 + 431 + 41 + + + + + + + + + URL + + + + + + + + + + + + + + + + + + 70 + 150 + 431 + 221 + + + + + + + URLs + + + + + + + + + + + + 140 + 110 + 271 + 36 + + + + + + + Add URL + + + + + + + Remove URL + + + + + + + + + + + diff --git a/forms/auto-scene-switcher.ui b/forms/auto-scene-switcher.ui deleted file mode 100644 index 391bee3..0000000 --- a/forms/auto-scene-switcher.ui +++ /dev/null @@ -1,219 +0,0 @@ - - - SceneSwitcher - - - - 0 - 0 - 310 - 380 - - - - SC2Switcher - - - - - 30 - 100 - 111 - 31 - - - - - - - - - - 150 - 10 - 141 - 22 - - - - - - - 150 - 40 - 141 - 22 - - - - - - - 30 - 10 - 111 - 21 - - - - In Game Scene: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 10 - 40 - 131 - 20 - - - - Out of Game Scene: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 10 - 210 - 131 - 16 - - - - SC2 PC IP Address: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 150 - 210 - 141 - 20 - - - - - - - 10 - 240 - 281 - 101 - - - - <html><head/><body><p>On your SC2 PC, open the Battle.net launcher, select SC2, click Options, Game Settings, and under SC2, check 'Additional Command Line Arguments', and paste this into the textbox:</p></body></html> - - - true - - - - - - 90 - 340 - 121 - 31 - - - - -clientapi 6119 - - - true - - - - - - 10 - 160 - 261 - 41 - - - - - 75 - true - - - - If SC2 is running on a different PC: (optional) - - - true - - - - - - 40 - 70 - 101 - 20 - - - - Qt::RightToLeft - - - Replay Scene: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 150 - 70 - 141 - 22 - - - - - - - 20 - 140 - 261 - 20 - - - - Qt::Horizontal - - - - - - 170 - 100 - 111 - 31 - - - - Close - - - - - - diff --git a/tool-helpers.hpp b/obs-util.h similarity index 79% rename from tool-helpers.hpp rename to obs-util.h index 840e746..c343674 100644 --- a/tool-helpers.hpp +++ b/obs-util.h @@ -2,7 +2,6 @@ #include #include -#include static inline OBSWeakSource GetWeakSourceByName(const char *name) { @@ -17,11 +16,6 @@ static inline OBSWeakSource GetWeakSourceByName(const char *name) return weak; } -static inline OBSWeakSource GetWeakSourceByQString(const QString &name) -{ - return GetWeakSourceByName(name.toUtf8().constData()); -} - static inline std::string GetWeakSourceName(obs_weak_source_t *weak_source) { std::string name; @@ -33,4 +27,4 @@ static inline std::string GetWeakSourceName(obs_weak_source_t *weak_source) } return name; -} +} \ No newline at end of file diff --git a/sc2switcher.c b/sc2switcher.c deleted file mode 100644 index f0132f3..0000000 --- a/sc2switcher.c +++ /dev/null @@ -1,19 +0,0 @@ -#include - -OBS_DECLARE_MODULE() -OBS_MODULE_USE_DEFAULT_LOCALE("sc2switcher", "en-US") - - -void InitSceneSwitcher(); -void FreeSceneSwitcher(); - -bool obs_module_load(void) -{ - InitSceneSwitcher(); - return true; -} - -void obs_module_unload(void) -{ - FreeSceneSwitcher(); -} diff --git a/sc2switcher.cpp b/sc2switcher.cpp new file mode 100755 index 0000000..af7cee7 --- /dev/null +++ b/sc2switcher.cpp @@ -0,0 +1,48 @@ +#include +#include +#include + +#include "Config.h" +#include "SC2Data.h" +#include "SceneSwitcher.h" +#include "ScoreTracker.h" +#include "Webhook.h" +#include "forms/SettingsDialog.h" + +OBS_DECLARE_MODULE() + +SceneSwitcher* sw = nullptr; +ScoreTracker* st = nullptr; +Webhook* wh = nullptr; + +bool obs_module_load(void) { + // set up load/save + obs_frontend_add_save_callback(LoadSaveHandler, nullptr); + + //set up the settings dialog + QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction( + "SC2Switcher"); + auto cb = []() { + QMainWindow *window = + (QMainWindow*)obs_frontend_get_main_window(); + SettingsDialog sd(window); + sd.exec(); + }; + + action->connect(action, &QAction::triggered, cb); + + // sc2 + SC2Data::Instance = new SC2Data(); + + // listeners + sw = new SceneSwitcher(SC2Data::Instance); + st = new ScoreTracker(SC2Data::Instance); + wh = new Webhook(SC2Data::Instance); + + return true; +} + +void obs_module_unload(void) { + delete sw; + delete wh; +} diff --git a/ui_auto-scene-switcher.h b/ui_auto-scene-switcher.h deleted file mode 100644 index 3577389..0000000 --- a/ui_auto-scene-switcher.h +++ /dev/null @@ -1,133 +0,0 @@ -/******************************************************************************** -** Form generated from reading UI file 'auto-scene-switcher.ui' -** -** Created by: Qt User Interface Compiler version 5.8.0 -** -** WARNING! All changes made in this file will be lost when recompiling UI file! -********************************************************************************/ - -#ifndef UI_AUTO_2D_SCENE_2D_SWITCHER_H -#define UI_AUTO_2D_SCENE_2D_SWITCHER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -QT_BEGIN_NAMESPACE - -class Ui_SceneSwitcher -{ -public: - QPushButton *toggleStartButton; - QComboBox *inGameScene; - QComboBox *outGameScene; - QLabel *label; - QLabel *label_2; - QLabel *label_4; - QLineEdit *ipAddr; - QLabel *label_5; - QLineEdit *lineEdit_2; - QLabel *label_3; - QLabel *label_6; - QComboBox *replayScene; - QFrame *line; - QPushButton *exitButton; - - void setupUi(QDialog *SceneSwitcher) - { - if (SceneSwitcher->objectName().isEmpty()) - SceneSwitcher->setObjectName(QStringLiteral("SceneSwitcher")); - SceneSwitcher->resize(310, 380); - toggleStartButton = new QPushButton(SceneSwitcher); - toggleStartButton->setObjectName(QStringLiteral("toggleStartButton")); - toggleStartButton->setGeometry(QRect(30, 100, 111, 31)); - inGameScene = new QComboBox(SceneSwitcher); - inGameScene->setObjectName(QStringLiteral("inGameScene")); - inGameScene->setGeometry(QRect(150, 10, 141, 22)); - outGameScene = new QComboBox(SceneSwitcher); - outGameScene->setObjectName(QStringLiteral("outGameScene")); - outGameScene->setGeometry(QRect(150, 40, 141, 22)); - label = new QLabel(SceneSwitcher); - label->setObjectName(QStringLiteral("label")); - label->setGeometry(QRect(30, 10, 111, 21)); - label->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); - label_2 = new QLabel(SceneSwitcher); - label_2->setObjectName(QStringLiteral("label_2")); - label_2->setGeometry(QRect(10, 40, 131, 20)); - label_2->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); - label_4 = new QLabel(SceneSwitcher); - label_4->setObjectName(QStringLiteral("label_4")); - label_4->setGeometry(QRect(10, 210, 131, 16)); - label_4->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); - ipAddr = new QLineEdit(SceneSwitcher); - ipAddr->setObjectName(QStringLiteral("ipAddr")); - ipAddr->setGeometry(QRect(150, 210, 141, 20)); - label_5 = new QLabel(SceneSwitcher); - label_5->setObjectName(QStringLiteral("label_5")); - label_5->setGeometry(QRect(10, 240, 281, 101)); - label_5->setWordWrap(true); - lineEdit_2 = new QLineEdit(SceneSwitcher); - lineEdit_2->setObjectName(QStringLiteral("lineEdit_2")); - lineEdit_2->setGeometry(QRect(90, 340, 121, 31)); - lineEdit_2->setReadOnly(true); - label_3 = new QLabel(SceneSwitcher); - label_3->setObjectName(QStringLiteral("label_3")); - label_3->setGeometry(QRect(10, 160, 261, 41)); - QFont font; - font.setBold(true); - font.setWeight(75); - label_3->setFont(font); - label_3->setWordWrap(true); - label_6 = new QLabel(SceneSwitcher); - label_6->setObjectName(QStringLiteral("label_6")); - label_6->setGeometry(QRect(40, 70, 101, 20)); - label_6->setLayoutDirection(Qt::RightToLeft); - label_6->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); - replayScene = new QComboBox(SceneSwitcher); - replayScene->setObjectName(QStringLiteral("replayScene")); - replayScene->setGeometry(QRect(150, 70, 141, 22)); - line = new QFrame(SceneSwitcher); - line->setObjectName(QStringLiteral("line")); - line->setGeometry(QRect(20, 140, 261, 20)); - line->setFrameShape(QFrame::HLine); - line->setFrameShadow(QFrame::Sunken); - exitButton = new QPushButton(SceneSwitcher); - exitButton->setObjectName(QStringLiteral("exitButton")); - exitButton->setGeometry(QRect(170, 100, 111, 31)); - - retranslateUi(SceneSwitcher); - - QMetaObject::connectSlotsByName(SceneSwitcher); - } // setupUi - - void retranslateUi(QDialog *SceneSwitcher) - { - SceneSwitcher->setWindowTitle(QApplication::translate("SceneSwitcher", "SC2Switcher", Q_NULLPTR)); - toggleStartButton->setText(QString()); - label->setText(QApplication::translate("SceneSwitcher", "In Game Scene:", Q_NULLPTR)); - label_2->setText(QApplication::translate("SceneSwitcher", "Out of Game Scene:", Q_NULLPTR)); - label_4->setText(QApplication::translate("SceneSwitcher", "SC2 PC IP Address:", Q_NULLPTR)); - label_5->setText(QApplication::translate("SceneSwitcher", "

On your SC2 PC, open the Battle.net launcher, select SC2, click Options, Game Settings, and under SC2, check 'Additional Command Line Arguments', and paste this into the textbox:

", Q_NULLPTR)); - lineEdit_2->setText(QApplication::translate("SceneSwitcher", "-clientapi 6119", Q_NULLPTR)); - label_3->setText(QApplication::translate("SceneSwitcher", "If SC2 is running on a different PC: (optional)", Q_NULLPTR)); - label_6->setText(QApplication::translate("SceneSwitcher", "Replay Scene:", Q_NULLPTR)); - exitButton->setText(QApplication::translate("SceneSwitcher", "Close", Q_NULLPTR)); - } // retranslateUi - -}; - -namespace Ui { - class SceneSwitcher: public Ui_SceneSwitcher {}; -} // namespace Ui - -QT_END_NAMESPACE - -#endif // UI_AUTO_2D_SCENE_2D_SWITCHER_H