├── log.h ├── SceneSwitcher.h ├── APIState.h ├── SC2State.h ├── Observer.h ├── Webhook.h ├── SC2Data.h ├── obs-util.h ├── ScoreTracker.h ├── sc2switcher.cpp ├── CMakeLists.txt ├── Constants.h ├── forms ├── SettingsDialog.h ├── SettingsDialog.cpp └── SettingsDialog.ui ├── Config.cpp ├── README.md ├── SceneSwitcher.cpp ├── SC2State.cpp ├── SC2Data.cpp ├── Webhook.cpp ├── ScoreTracker.cpp └── Config.h /log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "Config.h" 5 | 6 | static void s2log(std::string message) { 7 | Config* cfg = Config::Current(); 8 | if(cfg->logging) { 9 | blog(LOG_INFO, std::string("[sc2switcher] " + message).c_str()); 10 | } 11 | } -------------------------------------------------------------------------------- /SceneSwitcher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Observer.h" 4 | #include "SC2State.h" 5 | 6 | class SceneSwitcher : public Observer { 7 | public: 8 | explicit SceneSwitcher(SC2Data *sc2); 9 | 10 | public slots: 11 | void notify(SC2State*& previous, SC2State*& current); 12 | std::string getName(); 13 | }; -------------------------------------------------------------------------------- /APIState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct player { 4 | const char* name; 5 | const char* type; 6 | const char* race; 7 | const char* result; 8 | }; 9 | 10 | struct APIState { 11 | std::vector activeScreens; 12 | bool isReplay; 13 | bool isRewind; 14 | double displayTime; 15 | std::vector players; 16 | }; 17 | -------------------------------------------------------------------------------- /SC2State.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Constants.h" 3 | #include "APIState.h" 4 | 5 | class SC2State : public QObject { 6 | Q_OBJECT 7 | 8 | public: 9 | explicit SC2State(QObject* parent = Q_NULLPTR); 10 | virtual ~SC2State(); 11 | int appState = 0; 12 | int gameState = 0; 13 | int menuState = 0; 14 | APIState fullState; 15 | void fromJSONString(std::string uiResponse, std::string gameResponse); 16 | void fillState(); 17 | }; -------------------------------------------------------------------------------- /Observer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "SC2Data.h" 6 | #include "SC2State.h" 7 | 8 | 9 | #include 10 | 11 | class Observer : public QObject { 12 | Q_OBJECT 13 | SC2Data *sc2; 14 | 15 | public: 16 | Observer(SC2Data *scdata, QObject* parent = Q_NULLPTR) 17 | : QObject(parent) { 18 | sc2 = scdata; 19 | sc2->attach(this); 20 | } 21 | virtual void notify(SC2State*& previous, SC2State*& current) = 0; 22 | virtual std::string getName() { return ""; }; 23 | }; -------------------------------------------------------------------------------- /Webhook.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Observer.h" 4 | #include "SC2State.h" 5 | #include 6 | 7 | class Webhook : public Observer { 8 | private: 9 | void sendRequest(SC2State*& game, std::string event); 10 | std::string getQueryStringFromSC2State(SC2State*& game, std::string event, CURL* curl); 11 | std::string getJSONStringFromSC2State(SC2State*& game, std::string event); 12 | 13 | public: 14 | explicit Webhook(SC2Data *sc2); 15 | 16 | public slots: 17 | void notify(SC2State*& previous, SC2State*& current); 18 | std::string getName(); 19 | 20 | }; -------------------------------------------------------------------------------- /SC2Data.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "SC2State.h" 8 | 9 | using std::vector; 10 | 11 | class Observer; 12 | class SC2Data : public QObject { 13 | Q_OBJECT 14 | 15 | private: 16 | SC2State* state; 17 | QTimer* timer; 18 | vector watchers; 19 | bool stopping; 20 | 21 | public: 22 | explicit SC2Data(QObject* parent = Q_NULLPTR); 23 | virtual ~SC2Data(); 24 | static SC2Data* Instance; 25 | void attach(Observer* obs); 26 | void stop(); 27 | 28 | public slots: 29 | void update(); 30 | }; -------------------------------------------------------------------------------- /obs-util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | static inline OBSWeakSource GetWeakSourceByName(const char *name) 7 | { 8 | OBSWeakSource weak; 9 | obs_source_t *source = obs_get_source_by_name(name); 10 | if (source) { 11 | weak = obs_source_get_weak_source(source); 12 | obs_weak_source_release(weak); 13 | obs_source_release(source); 14 | } 15 | 16 | return weak; 17 | } 18 | 19 | static inline std::string GetWeakSourceName(obs_weak_source_t *weak_source) 20 | { 21 | std::string name; 22 | 23 | obs_source_t *source = obs_weak_source_get_source(weak_source); 24 | if (source) { 25 | name = obs_source_get_name(source); 26 | obs_source_release(source); 27 | } 28 | 29 | return name; 30 | } -------------------------------------------------------------------------------- /ScoreTracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Observer.h" 5 | #include "SC2State.h" 6 | 7 | class ScoreTracker : public Observer { 8 | private: 9 | QTimer* timer; 10 | public: 11 | explicit ScoreTracker(SC2Data *sc2); 12 | static ScoreTracker* Current(); 13 | ~ScoreTracker(); 14 | std::map < 15 | std::string, 16 | std::map 17 | > scores; 18 | std::string getScoreString(); 19 | void recordScore(std::string race, std::string result); 20 | void handleButton(std::string race, std::string result, std::string name); 21 | void addRandomConfirmMessage(std::string result); 22 | void addConfirmMessage(player* playerA, player* playerB); 23 | 24 | public slots: 25 | void notify(SC2State*& previous, SC2State*& current); 26 | void updateText(); 27 | std::string getName(); 28 | }; -------------------------------------------------------------------------------- /sc2switcher.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "Config.h" 6 | #include "log.h" 7 | #include "SC2Data.h" 8 | #include "SceneSwitcher.h" 9 | #include "ScoreTracker.h" 10 | #include "Webhook.h" 11 | #include "forms/SettingsDialog.h" 12 | 13 | OBS_DECLARE_MODULE() 14 | 15 | SceneSwitcher* sw = nullptr; 16 | ScoreTracker* st = nullptr; 17 | Webhook* wh = nullptr; 18 | 19 | bool obs_module_load(void) { 20 | obs_frontend_add_save_callback(LoadSaveHandler, nullptr); 21 | 22 | //set up the settings dialog 23 | QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction( 24 | "SC2Switcher"); 25 | auto cb = []() { 26 | QMainWindow *window = 27 | (QMainWindow*)obs_frontend_get_main_window(); 28 | SettingsDialog sd(window); 29 | sd.exec(); 30 | }; 31 | 32 | action->connect(action, &QAction::triggered, cb); 33 | 34 | // sc2 35 | SC2Data::Instance = new SC2Data(); 36 | 37 | // listeners 38 | sw = new SceneSwitcher(SC2Data::Instance); 39 | st = new ScoreTracker(SC2Data::Instance); 40 | wh = new Webhook(SC2Data::Instance); 41 | 42 | return true; 43 | } 44 | 45 | void obs_module_unload(void) { 46 | Config::free(); 47 | delete sw; 48 | delete wh; 49 | } 50 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(sc2switcher3) 2 | include_directories(${OBS_JANSSON_INCLUDE_DIRS}) 3 | 4 | set(sc2switcher3_HEADERS 5 | ${sc2switcher3_HEADERS} 6 | SC2Data.h 7 | Observer.h 8 | SC2State.h 9 | Constants.h 10 | SceneSwitcher.h 11 | ScoreTracker.h 12 | Webhook.h 13 | Config.h 14 | forms/SettingsDialog.h 15 | log.h 16 | ) 17 | set(sc2switcher3_SOURCES 18 | ${sc2switcher3_SOURCES} 19 | sc2switcher.cpp 20 | SC2Data.cpp 21 | SC2State.cpp 22 | SceneSwitcher.cpp 23 | ScoreTracker.cpp 24 | Webhook.cpp 25 | Config.cpp 26 | forms/SettingsDialog.cpp 27 | ) 28 | set(sc2switcher3_UI 29 | ${sc2switcher3_UI} 30 | forms/SettingsDialog.ui 31 | ) 32 | set(sc2switcher3_PLATFORM_LIBS 33 | ${OBS_JANSSON_IMPORT} 34 | ) 35 | 36 | qt5_wrap_ui(sc2switcher3_UI_HEADERS 37 | ${sc2switcher3_UI} 38 | ${sc2switcher3_PLATFORM_UI}) 39 | 40 | add_library(sc2switcher3 MODULE 41 | ${sc2switcher3_HEADERS} 42 | ${sc2switcher3_SOURCES} 43 | ${sc2switcher3_UI_HEADERS} 44 | ${sc2switcher3_PLATFORM_SOURCES} 45 | ${sc2switcher3_PLATFORM_HEADERS} 46 | ) 47 | target_link_libraries(sc2switcher3 48 | ${sc2switcher3_PLATFORM_LIBS} 49 | obs-frontend-api 50 | Qt5::Widgets 51 | ${LIBCURL_LIBRARIES} 52 | libobs) 53 | 54 | install_obs_plugin(sc2switcher3) 55 | -------------------------------------------------------------------------------- /Constants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // the order is important with these as the labels are not 4 | // in a consistent order in sc2. the scene switcher will 5 | // use whichever it comes accross first. menu consts must 6 | // go first so that they match up to the menuLabels array 7 | // properly. this is kind of bad? 8 | const int MENU_SCORESCREEN = 0; 9 | const int MENU_PROFILE = 1; 10 | const int MENU_LOBBY = 2; 11 | const int MENU_HOME = 3; 12 | const int MENU_CAMPAIGN = 4; 13 | const int MENU_COLLECTION = 5; 14 | const int MENU_COOP = 6; 15 | const int MENU_CUSTOM = 7; 16 | const int MENU_REPLAYS = 8; 17 | const int MENU_VERSUS = 9; 18 | const int MENU_NONE = 10; 19 | 20 | const char* const menuLabels[] = { 21 | "ScreenScore/ScreenScore", 22 | "ScreenUserProfile/ScreenUserProfile", 23 | "ScreenBattleLobby/ScreenBattleLobby", 24 | "ScreenHome/ScreenHome", 25 | "ScreenSingle/ScreenSingle", 26 | "ScreenCollection/ScreenCollection", 27 | "ScreenCoopCampaign/ScreenCoopCampaign", 28 | "ScreenCustom/ScreenCustom", 29 | "ScreenReplay/ScreenReplay", 30 | "ScreenMultiplayer/ScreenMultiplayer", 31 | }; 32 | 33 | const int APP_INGAME = 11; 34 | const int APP_MENU = 12; 35 | const int APP_LOADING = 13; 36 | 37 | const int GAME_INGAME = 14; 38 | const int GAME_OBS = 15; 39 | const int GAME_REPLAY = 16; -------------------------------------------------------------------------------- /forms/SettingsDialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "ui_SettingsDialog.h" 4 | 5 | class SettingsDialog : public QDialog 6 | { 7 | Q_OBJECT 8 | 9 | public: 10 | explicit SettingsDialog(QWidget* parent = 0); 11 | ~SettingsDialog(); 12 | void closeEvent(QCloseEvent *event) override; 13 | 14 | public slots: 15 | void on_toggleStartButton_clicked(); 16 | void on_ipAddr_textChanged(const QString &name); 17 | void on_addUsernameButton_clicked(); 18 | void on_removeUsernameButton_clicked(); 19 | 20 | void on_userNames_itemSelectionChanged(); 21 | void on_recentUsernames_itemSelectionChanged(); 22 | void on_textSourceName_textChanged(const QString& text); 23 | void on_textTemplate_textChanged(); 24 | 25 | void on_inGameScene_currentTextChanged(const QString &name); 26 | void on_outGameScene_currentTextChanged(const QString &name); 27 | void on_replayScene_currentTextChanged(const QString& text); 28 | void on_obsScene_currentTextChanged(const QString &name); 29 | void on_replaysScene_currentTextChanged(const QString &name); 30 | void on_campaignScene_currentTextChanged(const QString &name); 31 | void on_homeScene_currentTextChanged(const QString &name); 32 | void on_collectionScene_currentTextChanged(const QString &name); 33 | void on_coopScene_currentTextChanged(const QString &name); 34 | void on_customScene_currentTextChanged(const QString &name); 35 | void on_lobbyScene_currentTextChanged(const QString &name); 36 | void on_scoreScreenScene_currentTextChanged(const QString &name); 37 | void on_profileScene_currentTextChanged(const QString &name); 38 | void on_versusScene_currentTextChanged(const QString &name); 39 | void on_switcherEnabled_stateChanged(int state); 40 | void on_scoresEnabled_stateChanged(int state); 41 | void on_popupsEnabled_stateChanged(int state); 42 | void on_webhookEnabled_stateChanged(int state); 43 | void on_clearSettings_stateChanged(int state); 44 | void on_logging_stateChanged(int state); 45 | void on_switchOnLoad_stateChanged(int state); 46 | 47 | void on_tWinPlus_clicked(); 48 | void on_tWinMinus_clicked(); 49 | void on_tLossPlus_clicked(); 50 | void on_tLossMinus_clicked(); 51 | void on_zWinPlus_clicked(); 52 | void on_zWinMinus_clicked(); 53 | void on_zLossPlus_clicked(); 54 | void on_zLossMinus_clicked(); 55 | void on_pWinPlus_clicked(); 56 | void on_pWinMinus_clicked(); 57 | void on_pLossPlus_clicked(); 58 | void on_pLossMinus_clicked(); 59 | 60 | void on_addURLButton_clicked(); 61 | void on_removeURLButton_clicked(); 62 | void on_webhookURLList_itemSelectionChanged(); 63 | 64 | 65 | private: 66 | Ui::SettingsDialog* ui; 67 | bool isLoading = false; 68 | }; -------------------------------------------------------------------------------- /Config.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Config.h" 4 | 5 | Config* Config::_instance = new Config(); 6 | 7 | static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) 8 | { 9 | ((std::string*)userp)->append((char*)contents, size * nmemb); 10 | return size * nmemb; 11 | } 12 | 13 | Config::Config() : 14 | ipAddr(std::string("localhost")), 15 | scoreString(std::string("vT: ${tw}-${tl}\nvZ: ${zw}-${zl}\nvP: ${pw}-${pl}")), 16 | isRunning(false), 17 | switcherEnabled(false), 18 | scoresEnabled(false), 19 | popupsEnabled(true) {} 20 | 21 | Config* Config::Current() { 22 | return _instance; 23 | } 24 | 25 | void Config::free(){ 26 | if (_instance) { 27 | obs_weak_source_release(_instance->inGameScene); 28 | obs_weak_source_release(_instance->outGameScene); 29 | obs_weak_source_release(_instance->replayScene); 30 | obs_weak_source_release(_instance->obsScene); 31 | obs_weak_source_release(_instance->menuScenes[MENU_SCORESCREEN]); 32 | obs_weak_source_release(_instance->menuScenes[MENU_PROFILE]); 33 | obs_weak_source_release(_instance->menuScenes[MENU_LOBBY]); 34 | obs_weak_source_release(_instance->menuScenes[MENU_HOME]); 35 | obs_weak_source_release(_instance->menuScenes[MENU_CAMPAIGN]); 36 | obs_weak_source_release(_instance->menuScenes[MENU_COLLECTION]); 37 | obs_weak_source_release(_instance->menuScenes[MENU_COOP]); 38 | obs_weak_source_release(_instance->menuScenes[MENU_CUSTOM]); 39 | obs_weak_source_release(_instance->menuScenes[MENU_REPLAYS]); 40 | obs_weak_source_release(_instance->menuScenes[MENU_VERSUS]); 41 | } 42 | } 43 | 44 | Config::~Config() { 45 | //delete _instance; 46 | 47 | 48 | } 49 | 50 | void Config::checkForUpdates() { 51 | CURL *curl; 52 | CURLcode res; 53 | std::string response; 54 | 55 | curl = curl_easy_init(); 56 | if (curl) { 57 | std::string reqURL = "https://api.github.com/repos/leigholiver/OBS-SC2Switcher/releases/latest"; 58 | 59 | struct curl_slist *chunk = NULL; 60 | 61 | curl_easy_setopt(curl, CURLOPT_URL, reqURL.c_str()); 62 | curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 500); 63 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); 64 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 65 | 66 | chunk = curl_slist_append(chunk, "User-Agent: OBS-SC2Switcher"); 67 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); 68 | 69 | res = curl_easy_perform(curl); 70 | curl_easy_cleanup(curl); 71 | if (res != CURLE_OK) { 72 | return; 73 | } 74 | } 75 | 76 | json_error_t error; 77 | json_t* root = json_loads(response.c_str(), 0, &error); 78 | if (!root) { 79 | return; 80 | } 81 | 82 | json_t* url = json_object_get(root, "tag_name"); 83 | const char *urlText = json_string_value(url); 84 | float latestVer = std::stof(urlText); 85 | float currentVer = 0.98; 86 | if(latestVer > currentVer) { 87 | json_t* url2 = json_object_get(root, "html_url"); 88 | const char *urlText2 = json_string_value(url2); 89 | updateURL = urlText2; 90 | 91 | json_t* patch = json_object_get(root, "body"); 92 | const char *patchText = json_string_value(patch); 93 | updateDescription = patchText; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OBS SC2Switcher 2 | 3 | ## [Download](https://github.com/leigholiver/OBS-SC2Switcher/releases/latest/) 4 | 5 | ## Installation: 6 | ### Windows 7 | (0.96 and below): [Install the dependencies](https://www.microsoft.com/en-us/download/details.aspx?id=40784) 8 | 9 | 64bit: copy `sc2switcher.dll` from the zip file into `c:/program files (x86)/obs-studio/obs-plugins/64bit` directory 10 | 11 | 32bit: copy `sc2switcher-32.dll` from the zip file into `c:/program files (x86)/obs-studio/obs-plugins/32bit` directory 12 | 13 | ### Linux 14 | 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 15 | 16 | ## Usage: 17 | In the Tools menu, click on SC2Switcher. 18 | 19 | ### Scene Switcher 20 | - Choose a scene to switch to in different menus/games 21 | - All are optional and will fall back to the in game/out of game scenes 22 | - The observing scene requires you to have entered your username in the 'usernames' tab 23 | 24 | ### Score Tracker 25 | - Enter the name of the text source you are using for your scores 26 | - The text source will be updated with your score (mostly) automatically 27 | - If you play against a random player the plugin will ask you for their race 28 | - If you are neither player or both players (ie barcodes), the plugin will ask which player you were 29 | - 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 30 | 31 | 32 | ### Game Webhook 33 | - 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 34 | - 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 35 | 36 | ``` 37 | event: 'enter' or 'exit', 38 | displayTime: ~, 39 | players: [ 40 | { 41 | 'name': ~, 42 | 'type': ~, 43 | 'race': 'Terr', 'Zerg' or 'Prot', 44 | 'result': 'Victory' or 'Defeat', 45 | 'isme': 'true' or 'false', 46 | }, 47 | ] 48 | scores: { 49 | 'Terr', 'Zerg' or 'Prot': { 50 | "Victory": ~, 51 | "Defeat": ~ 52 | }, 53 | } 54 | ``` 55 | - 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 56 | 57 | ## If you use a separate PC to stream: 58 | Enter the IP address of your SC2 computer in the SC2 PC IP box. 59 | 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. 60 | 61 | 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: 62 | `{"activeScreens":["ScreenBackgroundSC2/ScreenBackgroundSC2","ScreenReplay/ScreenReplay","ScreenNavigationSC2/ScreenNavigationSC2","ScreenForegroundSC2/ScreenForegroundSC2","ScreenBattlenet/ScreenBattlenet"]}`. 63 | 64 | ## Building from source: 65 | Make sure you have a version of obs-studio building properly [(instructions are here)](https://github.com/jp9000/obs-studio/wiki/Install-Instructions). 66 | 67 | Clone this repository into `[OBS Source Directory]/UI/frontend-plugins/SC2Switcher` 68 | 69 | Add the line `add_subdirectory(SC2Switcher)` to `[OBS Source Directory]/UI/frontend-plugins/CMakeLists.txt` 70 | 71 | Run CMake again and hit Configure, Generate, then Open Project 72 | 73 | -------------------------------------------------------------------------------- /SceneSwitcher.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "SC2Data.h" 10 | #include "Observer.h" 11 | #include "SC2State.h" 12 | #include "SceneSwitcher.h" 13 | #include "Constants.h" 14 | #include "Config.h" 15 | #include "obs-util.h" 16 | #include "log.h" 17 | 18 | SceneSwitcher::SceneSwitcher(SC2Data *sc2): Observer(sc2){ } 19 | std::string SceneSwitcher::getName() { return "Scene Switcher"; } 20 | 21 | void SceneSwitcher::notify(SC2State*& previous, SC2State*& current) { 22 | Config* cfg = Config::Current(); 23 | if(cfg->switcherEnabled) { 24 | OBSWeakSource scene; 25 | 26 | if(previous->appState != current->appState) { 27 | if(current->appState == APP_LOADING) { 28 | // pre game loading screen 29 | if (previous->appState == APP_MENU) { 30 | if(cfg->switchOnLoad) { 31 | s2log("Loading Screen, entering game. Switching to In Game Scene"); 32 | scene = cfg->inGameScene; 33 | } 34 | else { 35 | s2log("Loading Screen, entering game. Switch On Load Disabled - Not Switching"); 36 | } 37 | } 38 | } 39 | 40 | // entering game 41 | if (current->appState == APP_INGAME && previous->appState != APP_INGAME) { 42 | if (current->gameState == GAME_REPLAY) { 43 | s2log("Entered Replay. Switching to Replay Scene"); 44 | scene = cfg->replayScene; 45 | } 46 | else { 47 | s2log("Entered Game. Switching to In Game Scene"); 48 | scene = cfg->inGameScene; 49 | } 50 | } 51 | 52 | // leaving game 53 | if (current->appState == APP_MENU && previous->appState != APP_MENU) { 54 | s2log("Entered Menus. Finding Menu Scene"); 55 | OBSWeakSource tmpScene; 56 | 57 | s2log("current->menustate is set: " + std::to_string(current->menuState)); 58 | if (current->menuState == MENU_NONE) { 59 | s2log("Menu state is MENU_NONE. Switching to Out of Game Scene"); 60 | tmpScene = cfg->outGameScene; 61 | } 62 | else { 63 | s2log("Menustate not MENU_NONE, finding scene"); 64 | tmpScene = cfg->menuScenes[current->menuState]; 65 | if (!tmpScene) { 66 | s2log("No menu set, using out of game scene"); 67 | tmpScene = cfg->outGameScene; 68 | } 69 | } 70 | 71 | if(tmpScene) { 72 | s2log("Switching to specified menu Scene"); 73 | scene = tmpScene; 74 | } 75 | else { 76 | s2log("No menu scene specified, will not switch"); 77 | } 78 | 79 | } 80 | } 81 | 82 | if (current->appState == APP_MENU && current->menuState != previous->menuState) { 83 | s2log("Menu changed, finding scene"); 84 | OBSWeakSource tmpScene2; 85 | 86 | if (current->menuState == MENU_NONE) { 87 | s2log("Menu state is MENU_NONE. Switching to Out of Game Scene"); 88 | tmpScene2 = cfg->outGameScene; 89 | } 90 | else { 91 | s2log("Menustate not MENU_NONE, finding scene"); 92 | tmpScene2 = cfg->menuScenes[current->menuState]; 93 | } 94 | 95 | if(tmpScene2) { 96 | s2log("Switching to specified menu Scene"); 97 | scene = tmpScene2; 98 | } 99 | else { 100 | s2log("No menu scene specified, will not switch"); 101 | } 102 | } 103 | 104 | if (current->gameState == GAME_OBS && current->appState == APP_INGAME) { 105 | if(current->gameState == GAME_OBS) { 106 | s2log("Detected observer, switching to observer scene"); 107 | if(cfg->obsScene) { 108 | scene = cfg->obsScene; 109 | } 110 | else { 111 | s2log("No observer scene found, switching to in game scene"); 112 | scene = cfg->inGameScene; 113 | } 114 | } 115 | } 116 | 117 | // scene change if necesary 118 | if (scene) { 119 | std::string sceneName = GetWeakSourceName(scene); 120 | s2log("Attempting Scene Switch to: " + sceneName); 121 | 122 | obs_source_t *source = obs_weak_source_get_source(scene); 123 | obs_source_t *currentSource = obs_frontend_get_current_scene(); 124 | 125 | if (source && source != currentSource) { 126 | obs_frontend_set_current_scene(source); 127 | s2log("Switched Scenes"); 128 | } 129 | else { 130 | s2log("Could not switch scenes, scene does not exist or already in that scene."); 131 | } 132 | 133 | obs_source_release(currentSource); 134 | obs_source_release(source); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /SC2State.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "Constants.h" 7 | #include "Config.h" 8 | #include "SC2State.h" 9 | #include "APIState.h" 10 | 11 | #include "obs-util.h" 12 | 13 | const char* jsonToString(json_t * result, const char* name) { 14 | json_t * jsonName = json_object_get(result, name); 15 | if (NULL != jsonName && json_is_string(jsonName)) { 16 | return json_string_value(jsonName); 17 | } 18 | return nullptr; 19 | } 20 | 21 | 22 | /** 23 | AppState (GAME/MENU/LOADING) 24 | GameState (INGAME/OBS/REPLAY) 25 | MenuState (SCORESCREEN/PROFILE/HOME/CAMPAIGN/COOP/REPLAYS/VERSUS/CUSTOM/LOBBY/COLLECTION) 26 | **/ 27 | SC2State::SC2State(QObject* parent) 28 | : QObject(parent) 29 | { 30 | 31 | } 32 | 33 | SC2State::~SC2State() { 34 | } 35 | 36 | void SC2State::fromJSONString(std::string uiResponse, std::string gameResponse) { 37 | 38 | APIState api; 39 | 40 | json_error_t error; 41 | json_t* root = json_loads(uiResponse.c_str(), 0, &error); 42 | if (!root) { 43 | return; 44 | } 45 | json_t* screens = json_object_get(root, "activeScreens"); 46 | for (size_t i = 0; i < json_array_size(screens); i++) { 47 | json_t* obj_txt = json_array_get(screens, i);; 48 | const char* strText; 49 | if (NULL != obj_txt && json_is_string(obj_txt)) { 50 | strText = json_string_value(obj_txt); 51 | api.activeScreens.push_back(strText); 52 | } 53 | } 54 | 55 | root = json_loads(gameResponse.c_str(), 0, &error); 56 | if (!root) { 57 | return; 58 | } 59 | 60 | json_t* isReplay = json_object_get(root, "isReplay"); 61 | api.isReplay = (isReplay == json_true()); 62 | 63 | json_t* displayTime = json_object_get(root, "displayTime"); 64 | if(json_is_number(displayTime)) { 65 | double dp = json_real_value(displayTime); 66 | api.displayTime = dp; 67 | } 68 | 69 | 70 | json_t* players = json_object_get(root, "players"); 71 | for (size_t i = 0; i < json_array_size(players); i++) { 72 | player* p = new player; 73 | json_t* playerJSON = json_array_get(players, i); 74 | p->name = (jsonToString(playerJSON, "name") == nullptr? "" : jsonToString(playerJSON, "name")); 75 | p->type = (jsonToString(playerJSON, "type") == nullptr? "" : jsonToString(playerJSON, "type")); 76 | p->race = (jsonToString(playerJSON, "race") == nullptr? "" : jsonToString(playerJSON, "race")); 77 | p->result = (jsonToString(playerJSON, "result") == nullptr? "" : jsonToString(playerJSON, "result")); 78 | api.players.push_back(p); 79 | } 80 | 81 | fullState = api; 82 | fillState(); 83 | } 84 | 85 | void SC2State::fillState() { 86 | Config* cfg = Config::Current(); 87 | if (fullState.activeScreens.size() > 1) { 88 | bool found = false; 89 | int sceneIdx = 0; 90 | int max = sizeof(menuLabels) / sizeof(menuLabels[0]); 91 | 92 | while (!found && sceneIdx < max) { 93 | for (size_t i = 0; i < fullState.activeScreens.size(); i++) { 94 | std::string active = fullState.activeScreens[i]; 95 | std::string label = menuLabels[sceneIdx]; 96 | if (active == label) { 97 | menuState = sceneIdx; 98 | found = true; 99 | break; 100 | } 101 | } 102 | sceneIdx++; 103 | } 104 | if(!found) { 105 | menuState = MENU_NONE; 106 | } 107 | appState = APP_MENU; 108 | } 109 | else if (fullState.activeScreens.size() == 0) { 110 | // in game 111 | appState = APP_INGAME; 112 | gameState = GAME_INGAME; 113 | } 114 | else { 115 | // we are in the loading screen 116 | appState = APP_LOADING; 117 | } 118 | 119 | if(fullState.isReplay) { 120 | gameState = GAME_REPLAY; 121 | } 122 | 123 | bool usernameFound = false; 124 | for (size_t i = 0; i < fullState.players.size(); i++) { 125 | if (cfg->usernames.size() > 0) { 126 | // if this is one of our usernames 127 | if (std::find(cfg->usernames.begin(), cfg->usernames.end(), fullState.players[i]->name) != cfg->usernames.end()) { 128 | usernameFound = true; 129 | } 130 | } 131 | // if this username isnt already in our recents, add it 132 | if (std::find(cfg->recentUsernames.begin(), cfg->recentUsernames.end(), fullState.players[i]->name) == cfg->recentUsernames.end()) { 133 | cfg->recentUsernames.push_back(fullState.players[i]->name); 134 | 135 | // limit the size of the recent usernames list, it could get quite big 136 | if (cfg->recentUsernames.size() > 8) { 137 | cfg->recentUsernames.erase(cfg->recentUsernames.begin()); 138 | } 139 | } 140 | } 141 | 142 | if (!usernameFound && cfg->usernames.size() > 0) { 143 | gameState = GAME_OBS; 144 | } 145 | } 146 | 147 | 148 | -------------------------------------------------------------------------------- /SC2Data.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "SC2Data.h" 8 | #include "SC2State.h" 9 | #include "Observer.h" 10 | #include "Config.h" 11 | #include "log.h" 12 | 13 | QT_USE_NAMESPACE 14 | 15 | SC2Data* SC2Data::Instance = nullptr; 16 | 17 | SC2Data::SC2Data(QObject* parent) 18 | : QObject(parent) 19 | { 20 | stopping = false; 21 | timer = new QTimer(this); 22 | connect(timer, SIGNAL(timeout()), this, SLOT(update())); 23 | timer->start(1500); 24 | state = nullptr; 25 | } 26 | 27 | SC2Data::~SC2Data() { 28 | timer->stop(); 29 | stopping = true; 30 | //delete Instance; 31 | } 32 | 33 | void SC2Data::stop() { 34 | stopping = true; 35 | timer->stop(); 36 | } 37 | 38 | void SC2Data::attach(Observer *obs) { 39 | watchers.push_back(obs); 40 | } 41 | 42 | static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) 43 | { 44 | ((std::string*)userp)->append((char*)contents, size * nmemb); 45 | return size * nmemb; 46 | } 47 | 48 | void SC2Data::update() { 49 | Config* config = Config::Current(); 50 | if(config->isRunning) { 51 | std::string reqURL; 52 | std::string UIResponse; 53 | std::string gameResponse; 54 | 55 | reqURL = config->ipAddr; 56 | if(reqURL == "") { 57 | reqURL = "localhost"; 58 | } 59 | reqURL = "http://" + reqURL + ":6119/"; 60 | 61 | 62 | int still_running = 0; 63 | CURLM *multi_handle; 64 | multi_handle = curl_multi_init(); 65 | 66 | CURL *uihandle; 67 | uihandle = curl_easy_init(); 68 | if (!uihandle) { 69 | return; 70 | } 71 | curl_multi_add_handle(multi_handle, uihandle); 72 | curl_easy_setopt(uihandle, CURLOPT_URL, (reqURL + "ui").c_str()); 73 | curl_easy_setopt(uihandle, CURLOPT_TIMEOUT_MS, 500); 74 | curl_easy_setopt(uihandle, CURLOPT_WRITEFUNCTION, WriteCallback); 75 | curl_easy_setopt(uihandle, CURLOPT_WRITEDATA, &UIResponse); 76 | 77 | CURL *gamehandle; 78 | gamehandle = curl_easy_init(); 79 | if (!gamehandle) { 80 | return; 81 | } 82 | curl_multi_add_handle(multi_handle, gamehandle); 83 | curl_easy_setopt(gamehandle, CURLOPT_URL, (reqURL + "game").c_str()); 84 | curl_easy_setopt(gamehandle, CURLOPT_TIMEOUT_MS, 500); 85 | curl_easy_setopt(gamehandle, CURLOPT_WRITEFUNCTION, WriteCallback); 86 | curl_easy_setopt(gamehandle, CURLOPT_WRITEDATA, &gameResponse); 87 | 88 | 89 | curl_multi_perform(multi_handle, &still_running); 90 | while (still_running) { 91 | CURLMcode mc; /* curl_multi_wait() return code */ 92 | int numfds; 93 | mc = curl_multi_wait(multi_handle, NULL, 0, 100, &numfds); 94 | if (mc != CURLM_OK) { 95 | s2log("/curl Failed"); 96 | s2log(curl_multi_strerror(mc)); 97 | 98 | curl_multi_remove_handle(multi_handle, uihandle); 99 | curl_easy_cleanup(uihandle); 100 | curl_multi_remove_handle(multi_handle, gamehandle); 101 | curl_easy_cleanup(gamehandle); 102 | curl_multi_cleanup(multi_handle); 103 | 104 | return; 105 | } 106 | if (stopping) { 107 | curl_multi_remove_handle(multi_handle, uihandle); 108 | curl_easy_cleanup(uihandle); 109 | curl_multi_remove_handle(multi_handle, gamehandle); 110 | curl_easy_cleanup(gamehandle); 111 | curl_multi_cleanup(multi_handle); 112 | return; 113 | } 114 | QCoreApplication::processEvents(); 115 | curl_multi_perform(multi_handle, &still_running); 116 | } 117 | 118 | if (stopping) { 119 | return; 120 | } 121 | 122 | if (UIResponse == "") { 123 | s2log("no ui response"); 124 | return; 125 | } 126 | if (gameResponse == "") { 127 | s2log("no game response"); 128 | return; 129 | } 130 | 131 | s2log("requests finished"); 132 | s2log(UIResponse); 133 | s2log(gameResponse); 134 | 135 | // create an sc2state object 136 | SC2State* newState = new SC2State(); 137 | newState->fromJSONString(UIResponse, gameResponse); 138 | if(state == nullptr) { 139 | state = newState; 140 | } 141 | 142 | // figure out if we're rewinding 143 | bool stillInGame = state->appState == APP_INGAME && newState->appState == APP_INGAME; 144 | bool nowAReplay = !state->fullState.isReplay && newState->fullState.isReplay; 145 | bool rewinding = stillInGame && nowAReplay; 146 | newState->fullState.isRewind = rewinding; 147 | 148 | bool menuChanged = state->menuState != newState->menuState; 149 | bool gameChanged = state->gameState != newState->gameState; 150 | bool appChanged = state->appState != newState->appState; 151 | 152 | if (menuChanged || gameChanged || appChanged || rewinding) { 153 | s2log("state has changed, notifying"); 154 | for (size_t i = 0; i < watchers.size(); i++) { 155 | s2log("notifying " + watchers[i]->getName()); 156 | watchers[i]->notify(state, newState); 157 | } 158 | state = newState; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Webhook.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "SC2Data.h" 4 | #include "Observer.h" 5 | #include "SC2State.h" 6 | #include "Webhook.h" 7 | #include "Constants.h" 8 | #include "Config.h" 9 | #include "obs-util.h" 10 | #include "APIState.h" 11 | #include "ScoreTracker.h" 12 | #include "log.h" 13 | 14 | Webhook::Webhook(SC2Data *sc2): Observer(sc2){ } 15 | std::string Webhook::getName() { return "Webhook"; } 16 | 17 | void Webhook::notify(SC2State*& previous, SC2State*& current) { 18 | Config* cfg = Config::Current(); 19 | if(cfg->webhookEnabled) { 20 | std::string event = ""; 21 | // quit and rewind 22 | if(previous->gameState == GAME_INGAME && 23 | previous->appState == APP_INGAME && 24 | current->gameState == GAME_REPLAY) { 25 | if(current->fullState.players.size() == 2) { 26 | s2log("Quit/Rewind detected"); 27 | event = "exit"; 28 | } 29 | } 30 | 31 | // just quit 32 | if(current->appState != APP_INGAME && 33 | previous->appState == APP_INGAME) { 34 | if(current->fullState.players.size() == 2 && !current->fullState.isReplay) { 35 | s2log("Quit Event detected"); 36 | event = "exit"; 37 | } 38 | } 39 | 40 | if(current->appState == APP_INGAME && 41 | previous->appState != APP_INGAME && 42 | current->gameState != GAME_REPLAY) { 43 | s2log("Enter Game Event detected"); 44 | event = "enter"; 45 | } 46 | 47 | if (event != "") { 48 | sendRequest(current, event); 49 | } 50 | } 51 | } 52 | 53 | void Webhook::sendRequest(SC2State*& game, std::string event) { 54 | Config* cfg = Config::Current(); 55 | 56 | int still_running = 0; 57 | CURLM *multi_handle; 58 | multi_handle = curl_multi_init(); 59 | 60 | // vector of handles 61 | vector handles; 62 | 63 | for (std::string &url : cfg->webhookURLList) { 64 | s2log("Adding url: " + url); 65 | CURL *handle; 66 | handle = curl_easy_init(); 67 | if (handle) { 68 | curl_multi_add_handle(multi_handle, handle); 69 | handles.push_back(handle); 70 | std::string qdelim = "?"; 71 | std::size_t found = url.find(qdelim); 72 | if (found != std::string::npos) { 73 | qdelim = "&"; 74 | } 75 | 76 | std::string resp = getJSONStringFromSC2State(game, event); 77 | std::string c_url = url + qdelim + "json=" + curl_easy_escape(handle, resp.c_str(), 0); 78 | curl_easy_setopt(handle, CURLOPT_URL, c_url.c_str()); 79 | curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, 500); 80 | } 81 | } 82 | 83 | curl_multi_perform(multi_handle, &still_running); 84 | while(still_running) { 85 | CURLMcode mc; /* curl_multi_wait() return code */ 86 | int numfds; 87 | mc = curl_multi_wait(multi_handle, NULL, 0, 1000, &numfds); 88 | 89 | if(mc != CURLM_OK) { 90 | break; 91 | } 92 | curl_multi_perform(multi_handle, &still_running); 93 | } 94 | s2log("requests finished"); 95 | 96 | // clean up each handle 97 | for (CURL* &handle : handles) { 98 | curl_multi_remove_handle(multi_handle, handle); 99 | curl_easy_cleanup(handle); 100 | } 101 | curl_multi_cleanup(multi_handle); 102 | } 103 | 104 | std::string Webhook::getQueryStringFromSC2State(SC2State*& game, std::string event, CURL* curl) { 105 | Config* cfg = Config::Current(); 106 | std::string resp = ""; 107 | for(size_t i=0; ifullState.players.size(); i++) { 108 | std::string index = std::to_string(i); 109 | resp = resp + "&players[" + index + "][name]=" + curl_easy_escape(curl, game->fullState.players[i]->name, 0); 110 | resp = resp + "&players[" + index + "][type]=" + curl_easy_escape(curl, game->fullState.players[i]->type, 0); 111 | resp = resp + "&players[" + index + "][race]=" + curl_easy_escape(curl, game->fullState.players[i]->race, 0); 112 | resp = resp + "&players[" + index + "][result]=" + curl_easy_escape(curl, game->fullState.players[i]->result, 0); 113 | if (std::find(cfg->usernames.begin(), cfg->usernames.end(), game->fullState.players[i]->name) != cfg->usernames.end()) { 114 | resp = resp + "&players[" + index + "][isme]=true"; 115 | } 116 | else { 117 | resp = resp + "&players[" + index + "][isme]=false"; 118 | } 119 | } 120 | 121 | std::string dp = std::to_string(game->fullState.displayTime); 122 | resp = resp + "&displayTime=" + dp; 123 | resp = resp + "&event=" + event; 124 | return resp; 125 | } 126 | 127 | std::string Webhook::getJSONStringFromSC2State(SC2State*& game, std::string event) { 128 | // i know this is dirty but im just testing it as the query string option 129 | // doesnt work with node. ill update it... 130 | Config* cfg = Config::Current(); 131 | std::string resp = ""; 132 | resp = resp + "{\"players\": ["; 133 | for(size_t i=0; ifullState.players.size(); i++) { 134 | resp = resp + "{"; 135 | resp = resp + "\"name\": \"" + game->fullState.players[i]->name + "\","; 136 | resp = resp + "\"type\": \"" + game->fullState.players[i]->type + "\","; 137 | resp = resp + "\"race\": \"" + game->fullState.players[i]->race + "\","; 138 | resp = resp + "\"result\": \"" + game->fullState.players[i]->result + "\","; 139 | 140 | if (std::find(cfg->usernames.begin(), cfg->usernames.end(), game->fullState.players[i]->name) != cfg->usernames.end()) { 141 | resp = resp + "\"isme\": true"; 142 | } 143 | else { 144 | resp = resp + "\"isme\": false"; 145 | } 146 | resp = resp + "}"; 147 | if(i + 1 != game->fullState.players.size()) { 148 | resp = resp + ","; 149 | } 150 | } 151 | resp = resp + "],"; 152 | std::string dp = std::to_string(game->fullState.displayTime); 153 | resp = resp + "\"displayTime\": \"" + dp + "\","; 154 | resp = resp + "\"event\": \"" + event + "\","; 155 | 156 | ScoreTracker* st = ScoreTracker::Current(); 157 | resp = resp + "\"scores\": { "; 158 | resp = resp + "\"Terr\": {\"Victory\": " + std::to_string(st->scores["Terr"]["Victory"]) + ", \"Defeat\": " + std::to_string(st->scores["Terr"]["Defeat"]) + " },"; 159 | resp = resp + "\"Prot\": {\"Victory\": " + std::to_string(st->scores["Prot"]["Victory"]) + ", \"Defeat\": " + std::to_string(st->scores["Prot"]["Defeat"]) + " },"; 160 | resp = resp + "\"Zerg\": {\"Victory\": " + std::to_string(st->scores["Zerg"]["Victory"]) + ", \"Defeat\": " + std::to_string(st->scores["Zerg"]["Defeat"]) + " }"; 161 | resp = resp + "}"; 162 | 163 | resp = resp + "}"; 164 | return resp; 165 | } 166 | -------------------------------------------------------------------------------- /ScoreTracker.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "SC2Data.h" 10 | #include "Observer.h" 11 | #include "SC2State.h" 12 | #include "ScoreTracker.h" 13 | #include "Constants.h" 14 | #include "Config.h" 15 | #include "APIState.h" 16 | #include "obs-util.h" 17 | 18 | ScoreTracker* _instance = nullptr; 19 | 20 | ScoreTracker* ScoreTracker::Current() { 21 | return _instance; 22 | } 23 | 24 | ScoreTracker::~ScoreTracker() { 25 | delete _instance; 26 | } 27 | 28 | ScoreTracker::ScoreTracker(SC2Data *sc2): Observer(sc2){ 29 | timer = new QTimer(this); 30 | QObject::connect(timer, &QTimer::timeout, this, [=]{ updateText(); }); 31 | timer->start(1000); 32 | _instance = this; 33 | } 34 | 35 | std::string ScoreTracker::getName() { return "Score Tracker"; } 36 | 37 | void ScoreTracker::notify(SC2State*& previous, SC2State*& current) { 38 | Config* cfg = Config::Current(); 39 | if(cfg->scoresEnabled && current->fullState.players.size() == 2) { 40 | bool leftGame = previous->appState != APP_MENU && 41 | current->appState == APP_MENU && 42 | !current->fullState.isReplay; 43 | 44 | if (leftGame || current->fullState.isRewind) { 45 | bool iAmPlayerA = false; 46 | bool iAmPlayerB = false; 47 | for (size_t i = 0; i < current->fullState.players.size(); i++) { 48 | for (size_t j= 0; j < cfg->usernames.size(); j++) { 49 | bool found = current->fullState.players[i]->name == cfg->usernames[j]; 50 | if(found && i==0) { 51 | iAmPlayerA = true; 52 | } 53 | if(found && i==1) { 54 | iAmPlayerB = true; 55 | } 56 | } 57 | } 58 | if (iAmPlayerA && iAmPlayerB) { 59 | // sc2 only tells us the name and barcodes are a thing.common names could also cause stats to 60 | // be recorded incorrectly.only way to get accurate info is asking the user to confirm 61 | addConfirmMessage(current->fullState.players[0], current->fullState.players[1]); 62 | } 63 | else if (!iAmPlayerA && !iAmPlayerB) { 64 | // we didnt know which player the user was so we have to ask 65 | addConfirmMessage(current->fullState.players[0], current->fullState.players[1]); 66 | } 67 | else { 68 | // normal result 69 | if (iAmPlayerB) { 70 | recordScore(current->fullState.players[0]->race, current->fullState.players[1]->result); 71 | } 72 | else { 73 | recordScore(current->fullState.players[1]->race, current->fullState.players[0]->result); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | std::string ScoreTracker::getScoreString() { 81 | // this probably needs refactoring 82 | Config* cfg = Config::Current(); 83 | std::string output = cfg->scoreString; 84 | std::map > scoreMap; 85 | 86 | scoreMap[0]["search"] = "${tw}"; 87 | scoreMap[0]["replace"] = std::to_string(scores["Terr"]["Victory"]); 88 | scoreMap[1]["search"] = "${tl}"; 89 | scoreMap[1]["replace"] = std::to_string(scores["Terr"]["Defeat"]); 90 | 91 | scoreMap[2]["search"] = "${zw}"; 92 | scoreMap[2]["replace"] = std::to_string(scores["Zerg"]["Victory"]); 93 | scoreMap[3]["search"] = "${zl}"; 94 | scoreMap[3]["replace"] = std::to_string(scores["Zerg"]["Defeat"]); 95 | 96 | scoreMap[4]["search"] = "${pw}"; 97 | scoreMap[4]["replace"] = std::to_string(scores["Prot"]["Victory"]); 98 | scoreMap[5]["search"] = "${pl}"; 99 | scoreMap[5]["replace"] = std::to_string(scores["Prot"]["Defeat"]); 100 | 101 | for (size_t i = 0; i < scoreMap.size(); i++) { 102 | std::string search = scoreMap[i]["search"]; 103 | std::string replace = scoreMap[i]["replace"]; 104 | size_t pos = 0; 105 | while ((pos = output.find(search, pos)) != std::string::npos) { 106 | output.replace(pos, search.length(), replace); 107 | pos += replace.length(); 108 | } 109 | } 110 | 111 | return output; 112 | } 113 | 114 | void ScoreTracker::updateText() { 115 | Config* cfg = Config::Current(); 116 | obs_source_t *source; 117 | try { 118 | source = obs_get_source_by_name(cfg->textSourceName.c_str()); 119 | } 120 | catch (...) { 121 | return; 122 | } 123 | if (source) { 124 | obs_data_t *obj = obs_data_create(); 125 | obs_data_set_string(obj, "text", getScoreString().c_str()); 126 | obs_source_update(source, obj); 127 | obs_data_release(obj); 128 | } 129 | obs_source_release(source); 130 | } 131 | 132 | void ScoreTracker::recordScore(std::string race, std::string result) { 133 | if (race == "random") { 134 | addRandomConfirmMessage(result); 135 | } 136 | else { 137 | scores[race][result] = scores[race][result] + 1; 138 | updateText(); 139 | } 140 | } 141 | 142 | void ScoreTracker::handleButton(std::string race, std::string result, std::string name) { 143 | Config* cfg = Config::Current(); 144 | if (std::find(cfg->usernames.begin(), cfg->usernames.end(), name) == cfg->usernames.end()) { 145 | cfg->usernames.push_back(name); 146 | } 147 | recordScore(race, result); 148 | } 149 | 150 | void ScoreTracker::addRandomConfirmMessage(std::string result) { 151 | Config* cfg = Config::Current(); 152 | if(cfg->popupsEnabled) { 153 | QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window(); 154 | QMessageBox* msgBox = new QMessageBox(mainWindow); 155 | msgBox->setText("Which race was your opponent playing?"); 156 | 157 | QAbstractButton* pButtonT = msgBox->addButton("Terran", QMessageBox::ActionRole); 158 | QObject::connect(pButtonT, &QPushButton::released, this, [=]{ 159 | handleButton("Terr", result, ""); 160 | }); 161 | 162 | QAbstractButton* pButtonZ = msgBox->addButton("Zerg", QMessageBox::ActionRole); 163 | QObject::connect(pButtonZ, &QPushButton::released, this, [=]{ 164 | handleButton("Zerg", result, ""); 165 | }); 166 | 167 | QAbstractButton* pButtonP = msgBox->addButton("Protoss", QMessageBox::ActionRole); 168 | QObject::connect(pButtonP, &QPushButton::released, this, [=]{ 169 | handleButton("Prot", result, ""); 170 | }); 171 | msgBox->setWindowModality(Qt::NonModal); 172 | msgBox->setAttribute(Qt::WA_ShowWithoutActivating); 173 | msgBox->setWindowFlags(Qt::Tool); 174 | msgBox->open(); 175 | } 176 | else { 177 | // find another way to handle them that doesnt steal focus 178 | } 179 | } 180 | 181 | 182 | void ScoreTracker::addConfirmMessage(player* playerA, player* playerB) { 183 | Config* cfg = Config::Current(); 184 | if(cfg->popupsEnabled) { 185 | QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window(); 186 | QMessageBox* msgBox = new QMessageBox(mainWindow); 187 | msgBox->setText("Which player were you?"); 188 | 189 | char playerAButton[50]; 190 | sprintf(playerAButton, "%s: %s (%s)", playerA->race, playerA->name, playerA->result); 191 | 192 | QAbstractButton* pButtonT = msgBox->addButton(playerAButton, QMessageBox::ActionRole); 193 | QObject::connect(pButtonT, &QPushButton::released, this, [=]{ 194 | handleButton(playerB->race, playerA->result, playerA->name); 195 | }); 196 | 197 | char playerBButton[50]; 198 | sprintf(playerBButton, "%s: %s (%s)", playerB->race, playerB->name, playerB->result); 199 | QAbstractButton* pButtonZ = msgBox->addButton(playerBButton, QMessageBox::ActionRole); 200 | QObject::connect(pButtonZ, &QPushButton::released, this, [=]{ 201 | handleButton(playerA->race, playerB->result, playerB->name); 202 | }); 203 | 204 | msgBox->setWindowModality(Qt::NonModal); 205 | msgBox->setAttribute(Qt::WA_ShowWithoutActivating); 206 | msgBox->setWindowFlags(Qt::Tool); 207 | msgBox->open(); 208 | } 209 | else { 210 | // find another way to handle them that doesnt steal focus 211 | } 212 | 213 | } 214 | 215 | -------------------------------------------------------------------------------- /Config.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "Constants.h" 9 | #include "obs-util.h" 10 | #include 11 | 12 | class Config { 13 | public: 14 | Config(); 15 | ~Config(); 16 | static Config* Current(); 17 | static void free(); 18 | 19 | // sc2data 20 | std::string ipAddr; 21 | std::vector usernames; 22 | std::vector recentUsernames; 23 | 24 | // scene switcher 25 | OBSWeakSource inGameScene; 26 | OBSWeakSource outGameScene; 27 | OBSWeakSource replayScene; 28 | OBSWeakSource obsScene; 29 | OBSWeakSource menuScenes[10]; 30 | 31 | // score tracker 32 | std::string textSourceName; 33 | std::string scoreString; 34 | 35 | bool isRunning; 36 | 37 | std::string updateURL; 38 | std::string updateDescription; 39 | void checkForUpdates(); 40 | bool logging = false; 41 | bool switchOnLoad = true; 42 | bool webhookEnabled = false; 43 | bool clearSettings = false; 44 | 45 | bool switcherEnabled; 46 | bool scoresEnabled; 47 | bool popupsEnabled; 48 | 49 | std::vector webhookURLList; 50 | 51 | 52 | private: 53 | static Config* _instance; 54 | }; 55 | 56 | 57 | static void LoadSaveHandler(obs_data_t *save_data, bool saving, void *) { 58 | if (saving) { 59 | Config* cfg = Config::Current(); 60 | if(!cfg->clearSettings) { 61 | obs_data_t *obj = obs_data_create(); 62 | 63 | // settings 64 | obs_data_set_bool(obj, "is_running", cfg->isRunning); 65 | obs_data_set_bool(obj, "switcher_enabled", cfg->switcherEnabled); 66 | obs_data_set_bool(obj, "scores_enabled", cfg->scoresEnabled); 67 | obs_data_set_bool(obj, "popups_enabled", cfg->popupsEnabled); 68 | obs_data_set_string(obj, "ip_addr", cfg->ipAddr.c_str()); 69 | obs_data_set_bool(obj, "webhook_enabled", cfg->webhookEnabled); 70 | obs_data_set_bool(obj, "logging", cfg->logging); 71 | obs_data_set_bool(obj, "switch_on_load", cfg->switchOnLoad); 72 | 73 | obs_data_array_t *array = obs_data_array_create(); 74 | for (std::string &s : cfg->webhookURLList) { 75 | obs_data_t *array_obj = obs_data_create(); 76 | obs_data_set_string(array_obj, "URL", s.c_str()); 77 | obs_data_array_push_back(array, array_obj); 78 | obs_data_release(array_obj); 79 | } 80 | obs_data_set_array(obj, "webhookURLs", array); 81 | obs_data_array_release(array); 82 | 83 | obs_data_array_t *array2 = obs_data_array_create(); 84 | for (std::string &s : cfg->usernames) { 85 | obs_data_t *array_obj = obs_data_create(); 86 | obs_data_set_string(array_obj, "username", s.c_str()); 87 | obs_data_array_push_back(array2, array_obj); 88 | obs_data_release(array_obj); 89 | } 90 | obs_data_set_array(obj, "usernames", array2); 91 | obs_data_array_release(array2); 92 | 93 | obs_data_set_string(obj, "textSourceName", cfg->textSourceName.c_str()); 94 | obs_data_set_string(obj, "scoreString", cfg->scoreString.c_str()); 95 | 96 | // scenes 97 | obs_data_set_string(obj, "in_game_scene", GetWeakSourceName(cfg->inGameScene).c_str()); 98 | obs_data_set_string(obj, "out_game_scene", GetWeakSourceName(cfg->outGameScene).c_str()); 99 | obs_data_set_string(obj, "replay_scene", GetWeakSourceName(cfg->replayScene).c_str()); 100 | obs_data_set_string(obj, "obs_scene", GetWeakSourceName(cfg->obsScene).c_str()); 101 | obs_data_set_string(obj, "MENU_SCORESCREEN", GetWeakSourceName(cfg->menuScenes[MENU_SCORESCREEN]).c_str()); 102 | obs_data_set_string(obj, "MENU_PROFILE", GetWeakSourceName(cfg->menuScenes[MENU_PROFILE]).c_str()); 103 | obs_data_set_string(obj, "MENU_LOBBY", GetWeakSourceName(cfg->menuScenes[MENU_LOBBY]).c_str()); 104 | obs_data_set_string(obj, "MENU_HOME", GetWeakSourceName(cfg->menuScenes[MENU_HOME]).c_str()); 105 | obs_data_set_string(obj, "MENU_CAMPAIGN", GetWeakSourceName(cfg->menuScenes[MENU_CAMPAIGN]).c_str()); 106 | obs_data_set_string(obj, "MENU_COLLECTION", GetWeakSourceName(cfg->menuScenes[MENU_COLLECTION]).c_str()); 107 | obs_data_set_string(obj, "MENU_COOP", GetWeakSourceName(cfg->menuScenes[MENU_COOP]).c_str()); 108 | obs_data_set_string(obj, "MENU_CUSTOM", GetWeakSourceName(cfg->menuScenes[MENU_CUSTOM]).c_str()); 109 | obs_data_set_string(obj, "MENU_REPLAYS", GetWeakSourceName(cfg->menuScenes[MENU_REPLAYS]).c_str()); 110 | obs_data_set_string(obj, "MENU_VERSUS", GetWeakSourceName(cfg->menuScenes[MENU_VERSUS]).c_str()); 111 | 112 | obs_data_set_obj(save_data, "sc2switcher2", obj); 113 | obs_data_release(obj); 114 | } 115 | } 116 | else { 117 | Config* cfg = Config::Current(); 118 | 119 | obs_data_t *obj = obs_data_get_obj(save_data, "sc2switcher2"); 120 | if (!obj) { 121 | obj = obs_data_create(); 122 | } 123 | 124 | cfg->ipAddr = obs_data_get_string(obj, "ip_addr"); 125 | 126 | std::vector emptyusernames; 127 | cfg->usernames = emptyusernames; 128 | obs_data_array_t *array = obs_data_get_array(obj, "usernames"); 129 | size_t count = obs_data_array_count(array); 130 | std::vector seen; 131 | for (size_t i = 0; i < count; i++) { 132 | obs_data_t *array_obj = obs_data_array_item(array, i); 133 | std::string un = obs_data_get_string(array_obj, "username"); 134 | if (std::find(seen.begin(), seen.end(), un) == seen.end() && un != "") { 135 | cfg->usernames.push_back(un); 136 | } 137 | obs_data_release(array_obj); 138 | } 139 | obs_data_array_release(array); 140 | 141 | std::vector emptywebhookURLList; 142 | cfg->webhookURLList = emptywebhookURLList; 143 | obs_data_array_t *array2 = obs_data_get_array(obj, "webhookURLs"); 144 | size_t count2 = obs_data_array_count(array2); 145 | std::vector seen2; 146 | for (size_t i = 0; i < count2; i++) { 147 | obs_data_t *array_obj = obs_data_array_item(array2, i); 148 | std::string url = obs_data_get_string(array_obj, "URL"); 149 | if (std::find(seen2.begin(), seen2.end(), url) == seen2.end() && url != "") { 150 | cfg->webhookURLList.push_back(url); 151 | } 152 | obs_data_release(array_obj); 153 | } 154 | obs_data_array_release(array2); 155 | 156 | cfg->textSourceName = obs_data_get_string(obj, "textSourceName"); 157 | 158 | 159 | std::string scoreString = obs_data_get_string(obj, "scoreString"); 160 | if (scoreString != "") { 161 | cfg->scoreString = scoreString; 162 | } 163 | 164 | cfg->isRunning = obs_data_get_bool(obj, "is_running"); 165 | cfg->switcherEnabled = obs_data_get_bool(obj, "switcher_enabled"); 166 | cfg->scoresEnabled = obs_data_get_bool(obj, "scores_enabled"); 167 | cfg->popupsEnabled = obs_data_get_bool(obj, "popups_enabled"); 168 | cfg->webhookEnabled = obs_data_get_bool(obj, "webhook_enabled"); 169 | cfg->logging = obs_data_get_bool(obj, "logging"); 170 | cfg->switchOnLoad = obs_data_get_bool(obj, "switch_on_load"); 171 | 172 | 173 | obs_weak_source_release(cfg->inGameScene); 174 | obs_weak_source_release(cfg->outGameScene); 175 | obs_weak_source_release(cfg->replayScene); 176 | obs_weak_source_release(cfg->obsScene); 177 | obs_weak_source_release(cfg->menuScenes[MENU_SCORESCREEN]); 178 | obs_weak_source_release(cfg->menuScenes[MENU_PROFILE]); 179 | obs_weak_source_release(cfg->menuScenes[MENU_LOBBY]); 180 | obs_weak_source_release(cfg->menuScenes[MENU_HOME]); 181 | obs_weak_source_release(cfg->menuScenes[MENU_CAMPAIGN]); 182 | obs_weak_source_release(cfg->menuScenes[MENU_COLLECTION]); 183 | obs_weak_source_release(cfg->menuScenes[MENU_COOP]); 184 | obs_weak_source_release(cfg->menuScenes[MENU_CUSTOM]); 185 | obs_weak_source_release(cfg->menuScenes[MENU_REPLAYS]); 186 | obs_weak_source_release(cfg->menuScenes[MENU_VERSUS]); 187 | 188 | 189 | cfg->inGameScene = GetWeakSourceByName(obs_data_get_string(obj, "in_game_scene")); 190 | cfg->outGameScene = GetWeakSourceByName(obs_data_get_string(obj, "out_game_scene")); 191 | cfg->replayScene = GetWeakSourceByName(obs_data_get_string(obj, "replay_scene")); 192 | cfg->obsScene = GetWeakSourceByName(obs_data_get_string(obj, "obs_scene")); 193 | cfg->menuScenes[MENU_SCORESCREEN] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_SCORESCREEN")); 194 | cfg->menuScenes[MENU_PROFILE] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_PROFILE")); 195 | cfg->menuScenes[MENU_LOBBY] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_LOBBY")); 196 | cfg->menuScenes[MENU_HOME] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_HOME")); 197 | cfg->menuScenes[MENU_CAMPAIGN] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_CAMPAIGN")); 198 | cfg->menuScenes[MENU_COLLECTION] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_COLLECTION")); 199 | cfg->menuScenes[MENU_COOP] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_COOP")); 200 | cfg->menuScenes[MENU_CUSTOM] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_CUSTOM")); 201 | cfg->menuScenes[MENU_REPLAYS] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_REPLAYS")); 202 | cfg->menuScenes[MENU_VERSUS] = GetWeakSourceByName(obs_data_get_string(obj, "MENU_VERSUS")); 203 | 204 | 205 | obs_data_release(obj); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /forms/SettingsDialog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ScoreTracker.h" 5 | #include "SettingsDialog.h" 6 | #include "Config.h" 7 | #include "Constants.h" 8 | #include "obs-util.h" 9 | 10 | Config* config = Config::Current(); 11 | 12 | SettingsDialog::SettingsDialog(QWidget* parent) : 13 | QDialog(parent, Qt::Dialog), 14 | ui(new Ui::SettingsDialog) 15 | { 16 | isLoading = true; 17 | ui->setupUi(this); 18 | 19 | // fill them boxes 20 | ui->inGameScene->addItem(""); 21 | ui->outGameScene->addItem(""); 22 | ui->replayScene->addItem(""); 23 | 24 | ui->obsScene->addItem(""); 25 | ui->replaysScene->addItem(""); 26 | ui->campaignScene->addItem(""); 27 | ui->homeScene->addItem(""); 28 | ui->collectionScene->addItem(""); 29 | ui->coopScene->addItem(""); 30 | ui->customScene->addItem(""); 31 | ui->lobbyScene->addItem(""); 32 | ui->scoreScreenScene->addItem(""); 33 | ui->profileScene->addItem(""); 34 | ui->versusScene->addItem(""); 35 | 36 | BPtr scenes = obs_frontend_get_scene_names(); 37 | char **temp = scenes; 38 | while (*temp) { 39 | const char *name = *temp; 40 | ui->inGameScene->addItem(name); 41 | ui->outGameScene->addItem(name); 42 | ui->replayScene->addItem(name); 43 | ui->obsScene->addItem(name); 44 | ui->replaysScene->addItem(name); 45 | ui->campaignScene->addItem(name); 46 | ui->homeScene->addItem(name); 47 | ui->collectionScene->addItem(name); 48 | ui->coopScene->addItem(name); 49 | ui->customScene->addItem(name); 50 | ui->lobbyScene->addItem(name); 51 | ui->scoreScreenScene->addItem(name); 52 | ui->profileScene->addItem(name); 53 | ui->versusScene->addItem(name); 54 | 55 | temp++; 56 | } 57 | 58 | ui->inGameScene->setCurrentText(GetWeakSourceName(config->inGameScene).c_str()); 59 | ui->outGameScene->setCurrentText(GetWeakSourceName(config->outGameScene).c_str()); 60 | ui->replayScene->setCurrentText(GetWeakSourceName(config->replayScene).c_str()); 61 | 62 | // there has GOT to be a better way to do this 63 | ui->obsScene->setCurrentText(GetWeakSourceName(config->obsScene).c_str()); 64 | ui->replaysScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_REPLAYS]).c_str()); 65 | ui->campaignScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_CAMPAIGN]).c_str()); 66 | ui->homeScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_HOME]).c_str()); 67 | ui->collectionScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_COLLECTION]).c_str()); 68 | ui->coopScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_COOP]).c_str()); 69 | ui->customScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_CUSTOM]).c_str()); 70 | ui->lobbyScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_LOBBY]).c_str()); 71 | ui->scoreScreenScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_SCORESCREEN]).c_str()); 72 | ui->profileScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_PROFILE]).c_str()); 73 | ui->versusScene->setCurrentText(GetWeakSourceName(config->menuScenes[MENU_VERSUS]).c_str()); 74 | 75 | for (size_t i = 0; i < config->usernames.size(); i++) { 76 | if(config->usernames[i] != "") { 77 | ui->userNames->addItem(config->usernames[i].c_str()); 78 | } 79 | } 80 | 81 | for (size_t i = 0; i < config->recentUsernames.size(); i++) { 82 | ui->recentUsernames->addItem(config->recentUsernames[i].c_str()); 83 | } 84 | 85 | ui->ipAddr->setText(QString::fromStdString(config->ipAddr)); 86 | 87 | config->checkForUpdates(); 88 | if(config->updateURL != "") { 89 | ui->downloadURL->setText(QString::fromStdString("updateURL + "\">Update Available. Click here to download.")); 90 | ui->downloadURL->setTextFormat(Qt::RichText); 91 | ui->downloadURL->setTextInteractionFlags(Qt::TextBrowserInteraction); 92 | ui->downloadURL->setOpenExternalLinks(true); 93 | ui->patchNotesLabel->setText(config->updateDescription.c_str()); 94 | } 95 | 96 | if (config->isRunning) { 97 | ui->toggleStartButton->setText("Stop"); 98 | } 99 | else { 100 | ui->toggleStartButton->setText("Start"); 101 | } 102 | 103 | ui->textTemplate->setText(QString::fromStdString(config->scoreString)); 104 | ui->textSourceName->setText(QString::fromStdString(config->textSourceName)); 105 | 106 | if(config->switcherEnabled) { 107 | ui->switcherEnabled->setChecked(true); 108 | } 109 | else { 110 | ui->switcherEnabled->setChecked(false); 111 | } 112 | if(config->scoresEnabled) { 113 | ui->scoresEnabled->setChecked(true); 114 | } 115 | else { 116 | ui->scoresEnabled->setChecked(false); 117 | } 118 | if(config->popupsEnabled) { 119 | ui->popupsEnabled->setChecked(true); 120 | } 121 | else { 122 | ui->popupsEnabled->setChecked(false); 123 | } 124 | if(config->clearSettings) { 125 | ui->clearSettings->setChecked(true); 126 | } 127 | else { 128 | ui->clearSettings->setChecked(false); 129 | } 130 | 131 | if(config->logging) { 132 | ui->logging->setChecked(true); 133 | } 134 | else { 135 | ui->logging->setChecked(false); 136 | } 137 | 138 | if(config->switchOnLoad) { 139 | ui->switchOnLoad->setChecked(true); 140 | } 141 | else { 142 | ui->switchOnLoad->setChecked(false); 143 | } 144 | 145 | if(config->webhookEnabled) { 146 | ui->webhookEnabled->setChecked(true); 147 | } 148 | 149 | for (size_t i = 0; i < config->webhookURLList.size(); i++) { 150 | ui->webhookURLList->addItem(config->webhookURLList[i].c_str()); 151 | } 152 | 153 | ScoreTracker* st = ScoreTracker::Current(); 154 | ui->scoresLabel->setText(st->getScoreString().c_str()); 155 | 156 | 157 | isLoading = false; 158 | } 159 | 160 | SettingsDialog::~SettingsDialog() { 161 | delete ui; 162 | } 163 | 164 | void SettingsDialog::closeEvent(QCloseEvent*) { 165 | //obs_frontend_save(); 166 | } 167 | 168 | 169 | void SettingsDialog::on_inGameScene_currentTextChanged(const QString& text) { 170 | if(!isLoading) { 171 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 172 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 173 | 174 | config->inGameScene = ws; 175 | 176 | obs_weak_source_release(ws); 177 | obs_source_release(scene); 178 | } 179 | } 180 | 181 | void SettingsDialog::on_outGameScene_currentTextChanged(const QString& text) { 182 | if(!isLoading) { 183 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 184 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 185 | 186 | config->outGameScene = ws; 187 | 188 | obs_weak_source_release(ws); 189 | obs_source_release(scene); 190 | } 191 | } 192 | 193 | void SettingsDialog::on_replayScene_currentTextChanged(const QString& text) { 194 | if(!isLoading) { 195 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 196 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 197 | 198 | config->replayScene = ws; 199 | 200 | obs_weak_source_release(ws); 201 | obs_source_release(scene); 202 | } 203 | } 204 | 205 | void SettingsDialog::on_ipAddr_textChanged(const QString& text) { 206 | if(!isLoading) { 207 | config->ipAddr = (std::string) text.toUtf8().constData(); 208 | } 209 | } 210 | 211 | void SettingsDialog::on_toggleStartButton_clicked() 212 | { 213 | if (config->isRunning) { 214 | config->isRunning = false; 215 | ui->toggleStartButton->setText("Start"); 216 | } 217 | else { 218 | config->isRunning = true; 219 | ui->toggleStartButton->setText("Stop"); 220 | } 221 | } 222 | 223 | void SettingsDialog::on_obsScene_currentTextChanged(const QString &text) { 224 | if(!isLoading) { 225 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 226 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 227 | 228 | config->obsScene = ws; 229 | 230 | obs_weak_source_release(ws); 231 | obs_source_release(scene); 232 | } 233 | } 234 | 235 | void SettingsDialog::on_replaysScene_currentTextChanged(const QString &text) { 236 | if(!isLoading) { 237 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 238 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 239 | 240 | config->menuScenes[MENU_REPLAYS] = ws; 241 | 242 | obs_weak_source_release(ws); 243 | obs_source_release(scene); 244 | } 245 | } 246 | 247 | void SettingsDialog::on_campaignScene_currentTextChanged(const QString &text) { 248 | if(!isLoading) { 249 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 250 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 251 | 252 | config->menuScenes[MENU_CAMPAIGN] = ws; 253 | 254 | obs_weak_source_release(ws); 255 | obs_source_release(scene); 256 | } 257 | } 258 | 259 | void SettingsDialog::on_homeScene_currentTextChanged(const QString &text) { 260 | if(!isLoading) { 261 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 262 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 263 | 264 | config->menuScenes[MENU_HOME] = ws; 265 | 266 | obs_weak_source_release(ws); 267 | obs_source_release(scene); 268 | } 269 | } 270 | 271 | void SettingsDialog::on_collectionScene_currentTextChanged(const QString &text) { 272 | if(!isLoading) { 273 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 274 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 275 | 276 | config->menuScenes[MENU_COLLECTION] = ws; 277 | 278 | obs_weak_source_release(ws); 279 | obs_source_release(scene); 280 | } 281 | 282 | } 283 | 284 | void SettingsDialog::on_coopScene_currentTextChanged(const QString &text) { 285 | if(!isLoading) { 286 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 287 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 288 | 289 | config->menuScenes[MENU_COOP] = ws; 290 | 291 | obs_weak_source_release(ws); 292 | obs_source_release(scene); 293 | } 294 | 295 | } 296 | 297 | void SettingsDialog::on_customScene_currentTextChanged(const QString &text) { 298 | if(!isLoading) { 299 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 300 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 301 | 302 | config->menuScenes[MENU_CUSTOM] = ws; 303 | 304 | obs_weak_source_release(ws); 305 | obs_source_release(scene); 306 | } 307 | } 308 | 309 | void SettingsDialog::on_lobbyScene_currentTextChanged(const QString &text) { 310 | if(!isLoading) { 311 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 312 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 313 | 314 | config->menuScenes[MENU_LOBBY] = ws; 315 | 316 | obs_weak_source_release(ws); 317 | obs_source_release(scene); 318 | } 319 | } 320 | 321 | void SettingsDialog::on_scoreScreenScene_currentTextChanged(const QString &text) { 322 | if(!isLoading) { 323 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 324 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 325 | 326 | config->menuScenes[MENU_SCORESCREEN] = ws; 327 | 328 | obs_weak_source_release(ws); 329 | obs_source_release(scene); 330 | } 331 | } 332 | 333 | void SettingsDialog::on_profileScene_currentTextChanged(const QString &text) { 334 | if(!isLoading) { 335 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 336 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 337 | 338 | config->menuScenes[MENU_PROFILE] = ws; 339 | 340 | obs_weak_source_release(ws); 341 | obs_source_release(scene); 342 | } 343 | } 344 | 345 | void SettingsDialog::on_versusScene_currentTextChanged(const QString &text) { 346 | if(!isLoading) { 347 | obs_source_t* scene = obs_get_source_by_name(text.toUtf8().constData()); 348 | obs_weak_source_t* ws = obs_source_get_weak_source(scene); 349 | 350 | config->menuScenes[MENU_VERSUS] = ws; 351 | 352 | obs_weak_source_release(ws); 353 | obs_source_release(scene); 354 | } 355 | } 356 | 357 | void SettingsDialog::on_addUsernameButton_clicked() { 358 | if(!isLoading) { 359 | // add the username to our usernames 360 | std::string username = ui->usernameLine->text().toUtf8().constData(); 361 | 362 | if (std::find(config->usernames.begin(), config->usernames.end(), username) == config->usernames.end() && username != "") { 363 | config->usernames.push_back(username); // add it to the ui list 364 | ui->userNames->addItem(ui->usernameLine->text()); 365 | } 366 | 367 | // clear the username input 368 | ui->usernameLine->setText(""); 369 | } 370 | } 371 | 372 | void SettingsDialog::on_removeUsernameButton_clicked() { 373 | if(!isLoading) { 374 | // remove the list item from our usernames 375 | config->usernames.erase(std::remove(config->usernames.begin(), config->usernames.end(), ui->usernameLine->text().toUtf8().constData()), config->usernames.end()); 376 | 377 | // remove the list item from the ui list 378 | // have to iterate backwards because the size of the list changes as items are removed 379 | // also store the name first as the value of usernameLine changes as items are removed and new items selected 380 | std::string searchName = ui->usernameLine->text().toUtf8().constData(); 381 | for (int i = ui->userNames->count()-1; i >= 0; i--) { 382 | QListWidgetItem *item = ui->userNames->item(i); 383 | if (item->text() == QString::fromStdString(searchName)) { 384 | int row = ui->userNames->row(item); 385 | delete ui->userNames->takeItem(row); 386 | } 387 | } 388 | 389 | // clear the username box if its empty 390 | // otherwise box will hold selected item 391 | if (ui->userNames->count() == 0) { 392 | ui->usernameLine->setText(""); 393 | } 394 | } 395 | } 396 | 397 | void SettingsDialog::on_userNames_itemSelectionChanged() { 398 | if(!isLoading) { 399 | if (ui->userNames->currentItem() != NULL && ui->userNames->currentItem()->text() != NULL) { 400 | ui->usernameLine->setText(ui->userNames->currentItem()->text()); 401 | } 402 | } 403 | } 404 | 405 | void SettingsDialog::on_recentUsernames_itemSelectionChanged() { 406 | if(!isLoading) { 407 | if (ui->recentUsernames->currentItem() != NULL && ui->recentUsernames->currentItem()->text() != NULL) { 408 | ui->usernameLine->setText(ui->recentUsernames->currentItem()->text()); 409 | } 410 | } 411 | } 412 | 413 | void SettingsDialog::on_textSourceName_textChanged(const QString& text) { 414 | if(!isLoading) { 415 | if (config->textSourceName != (std::string)text.toUtf8().constData()) { 416 | config->textSourceName = (std::string)text.toUtf8().constData(); 417 | } 418 | } 419 | } 420 | 421 | void SettingsDialog::on_textTemplate_textChanged() { 422 | if(!isLoading) { 423 | if (config->scoreString != (std::string)ui->textTemplate->toPlainText().toUtf8().constData()) { 424 | config->scoreString = (std::string)ui->textTemplate->toPlainText().toUtf8().constData(); 425 | } 426 | } 427 | } 428 | 429 | void SettingsDialog::on_switcherEnabled_stateChanged(int state) { 430 | if(!isLoading) { 431 | if(state == Qt::Checked) { 432 | config->switcherEnabled = true; 433 | } 434 | else { 435 | config->switcherEnabled = false; 436 | } 437 | } 438 | } 439 | 440 | void SettingsDialog::on_logging_stateChanged(int state) { 441 | if(!isLoading) { 442 | if(state == Qt::Checked) { 443 | config->logging = true; 444 | } 445 | else { 446 | config->logging = false; 447 | } 448 | } 449 | } 450 | 451 | void SettingsDialog::on_switchOnLoad_stateChanged(int state) { 452 | if(!isLoading) { 453 | if(state == Qt::Checked) { 454 | config->switchOnLoad = true; 455 | } 456 | else { 457 | config->switchOnLoad = false; 458 | } 459 | } 460 | } 461 | 462 | 463 | void SettingsDialog::on_scoresEnabled_stateChanged(int state) { 464 | if(!isLoading) { 465 | if(state == Qt::Checked) { 466 | config->scoresEnabled = true; 467 | } 468 | else { 469 | config->scoresEnabled = false; 470 | } 471 | } 472 | } 473 | 474 | void SettingsDialog::on_clearSettings_stateChanged(int state) { 475 | if(!isLoading) { 476 | if(state == Qt::Checked) { 477 | config->clearSettings = true; 478 | } 479 | else { 480 | config->clearSettings = false; 481 | } 482 | } 483 | } 484 | 485 | void SettingsDialog::on_popupsEnabled_stateChanged(int state) { 486 | if(!isLoading) { 487 | if(state == Qt::Checked) { 488 | config->popupsEnabled = true; 489 | } 490 | else { 491 | config->popupsEnabled = false; 492 | } 493 | } 494 | } 495 | 496 | 497 | void SettingsDialog::on_webhookEnabled_stateChanged(int state){ 498 | if(!isLoading) { 499 | if(state == Qt::Checked) { 500 | config->webhookEnabled = true; 501 | } 502 | else { 503 | config->webhookEnabled = false; 504 | } 505 | } 506 | } 507 | 508 | void SettingsDialog::on_tWinPlus_clicked() { 509 | ScoreTracker* st = ScoreTracker::Current(); 510 | if(!isLoading && st) { 511 | st->scores["Terr"]["Victory"] = st->scores["Terr"]["Victory"] + 1; 512 | st->updateText(); 513 | ui->scoresLabel->setText(st->getScoreString().c_str()); 514 | } 515 | } 516 | 517 | void SettingsDialog::on_tWinMinus_clicked() { 518 | ScoreTracker* st = ScoreTracker::Current(); 519 | if (!isLoading) { 520 | if (st->scores["Terr"]["Victory"] > 0) { 521 | st->scores["Terr"]["Victory"] = st->scores["Terr"]["Victory"] - 1; 522 | st->updateText(); 523 | ui->scoresLabel->setText(st->getScoreString().c_str()); 524 | } 525 | } 526 | } 527 | 528 | void SettingsDialog::on_tLossPlus_clicked() { 529 | ScoreTracker* st = ScoreTracker::Current(); 530 | if(!isLoading) { 531 | st->scores["Terr"]["Defeat"] = st->scores["Terr"]["Defeat"] + 1; 532 | st->updateText(); 533 | ui->scoresLabel->setText(st->getScoreString().c_str()); 534 | } 535 | } 536 | 537 | void SettingsDialog::on_tLossMinus_clicked() { 538 | ScoreTracker* st = ScoreTracker::Current(); 539 | if (!isLoading) { 540 | if (st->scores["Terr"]["Defeat"] > 0) { 541 | st->scores["Terr"]["Defeat"] = st->scores["Terr"]["Defeat"] - 1; 542 | st->updateText(); 543 | ui->scoresLabel->setText(st->getScoreString().c_str()); 544 | } 545 | } 546 | } 547 | 548 | void SettingsDialog::on_zWinPlus_clicked() { 549 | ScoreTracker* st = ScoreTracker::Current(); 550 | if(!isLoading) { 551 | st->scores["Zerg"]["Victory"] = st->scores["Zerg"]["Victory"] + 1; 552 | st->updateText(); 553 | ui->scoresLabel->setText(st->getScoreString().c_str()); 554 | } 555 | } 556 | 557 | void SettingsDialog::on_zWinMinus_clicked() { 558 | ScoreTracker* st = ScoreTracker::Current(); 559 | if (!isLoading) { 560 | if (st->scores["Zerg"]["Victory"] > 0) { 561 | st->scores["Zerg"]["Victory"] = st->scores["Zerg"]["Victory"] - 1; 562 | st->updateText(); 563 | ui->scoresLabel->setText(st->getScoreString().c_str()); 564 | } 565 | } 566 | } 567 | 568 | void SettingsDialog::on_zLossPlus_clicked() { 569 | ScoreTracker* st = ScoreTracker::Current(); 570 | if(!isLoading) { 571 | st->scores["Zerg"]["Defeat"] = st->scores["Zerg"]["Defeat"] + 1; 572 | st->updateText(); 573 | ui->scoresLabel->setText(st->getScoreString().c_str()); 574 | } 575 | } 576 | 577 | void SettingsDialog::on_zLossMinus_clicked() { 578 | ScoreTracker* st = ScoreTracker::Current(); 579 | if (!isLoading) { 580 | if (st->scores["Zerg"]["Defeat"] > 0) { 581 | st->scores["Zerg"]["Defeat"] = st->scores["Zerg"]["Defeat"] - 1; 582 | st->updateText(); 583 | ui->scoresLabel->setText(st->getScoreString().c_str()); 584 | } 585 | } 586 | } 587 | 588 | void SettingsDialog::on_pWinPlus_clicked() { 589 | ScoreTracker* st = ScoreTracker::Current(); 590 | if(!isLoading) { 591 | st->scores["Prot"]["Victory"] = st->scores["Prot"]["Victory"] + 1; 592 | st->updateText(); 593 | ui->scoresLabel->setText(st->getScoreString().c_str()); 594 | } 595 | } 596 | 597 | void SettingsDialog::on_pWinMinus_clicked() { 598 | ScoreTracker* st = ScoreTracker::Current(); 599 | if (!isLoading) { 600 | if (st->scores["Prot"]["Victory"] > 0) { 601 | st->scores["Prot"]["Victory"] = st->scores["Prot"]["Victory"] - 1; 602 | st->updateText(); 603 | ui->scoresLabel->setText(st->getScoreString().c_str()); 604 | } 605 | } 606 | } 607 | 608 | void SettingsDialog::on_pLossPlus_clicked() { 609 | ScoreTracker* st = ScoreTracker::Current(); 610 | if(!isLoading) { 611 | st->scores["Prot"]["Defeat"] = st->scores["Prot"]["Defeat"] + 1; 612 | st->updateText(); 613 | ui->scoresLabel->setText(st->getScoreString().c_str()); 614 | } 615 | } 616 | 617 | void SettingsDialog::on_pLossMinus_clicked() { 618 | ScoreTracker* st = ScoreTracker::Current(); 619 | if(!isLoading) { 620 | if (st->scores["Prot"]["Defeat"] > 0) { 621 | st->scores["Prot"]["Defeat"] = st->scores["Prot"]["Defeat"] - 1; 622 | st->updateText(); 623 | ui->scoresLabel->setText(st->getScoreString().c_str()); 624 | } 625 | } 626 | } 627 | 628 | 629 | void SettingsDialog::on_addURLButton_clicked() { 630 | if(!isLoading) { 631 | std::string username = ui->webhookEnterGame->text().toUtf8().constData(); 632 | config->webhookURLList.push_back(username); 633 | ui->webhookURLList->addItem(ui->webhookEnterGame->text()); 634 | ui->webhookEnterGame->setText(""); 635 | } 636 | } 637 | 638 | void SettingsDialog::on_removeURLButton_clicked() { 639 | if(!isLoading) { 640 | config->webhookURLList.erase(std::remove(config->webhookURLList.begin(), config->webhookURLList.end(), ui->webhookEnterGame->text().toUtf8().constData()), config->webhookURLList.end()); 641 | 642 | std::string searchName = ui->webhookEnterGame->text().toUtf8().constData(); 643 | for (int i = ui->webhookURLList->count()-1; i >= 0; i--) { 644 | QListWidgetItem *item = ui->webhookURLList->item(i); 645 | if (item->text() == QString::fromStdString(searchName)) { 646 | int row = ui->webhookURLList->row(item); 647 | delete ui->webhookURLList->takeItem(row); 648 | } 649 | } 650 | 651 | if (ui->webhookURLList->count() == 0) { 652 | ui->webhookEnterGame->setText(""); 653 | } 654 | } 655 | } 656 | 657 | void SettingsDialog::on_webhookURLList_itemSelectionChanged() { 658 | if(!isLoading) { 659 | if (ui->webhookURLList->currentItem() != NULL && ui->webhookURLList->currentItem()->text() != NULL) { 660 | ui->webhookEnterGame->setText(ui->webhookURLList->currentItem()->text()); 661 | } 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /forms/SettingsDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 590 10 | 419 11 | 12 | 13 | 14 | SC2Switcher 15 | 16 | 17 | 18 | 19 | 0 20 | 0 21 | 591 22 | 421 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | Settings 31 | 32 | 33 | 34 | 35 | 40 36 | 40 37 | 511 38 | 331 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Clear Settings On Next Restart 59 | 60 | 61 | 62 | 63 | 64 | 65 | Enable Debug Log 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 75 82 | true 83 | 84 | 85 | 86 | If SC2 is running on a different PC: (optional) 87 | 88 | 89 | true 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | SC2 PC IP Address: 99 | 100 | 101 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | <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> 118 | 119 | 120 | true 121 | 122 | 123 | 124 | 125 | 126 | 127 | -clientapi 6119 128 | 129 | 130 | true 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | Usernames 162 | 163 | 164 | 165 | 166 | 40 167 | 150 168 | 501 169 | 171 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | Usernames: 179 | 180 | 181 | 182 | 183 | 184 | 185 | QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | Recent Usernames: 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 140 211 | 20 212 | 301 213 | 100 214 | 215 | 216 | 217 | 218 | 219 | 220 | Add Username: 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | Add 233 | 234 | 235 | 236 | 237 | 238 | 239 | Remove 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | Scene Switcher 251 | 252 | 253 | 254 | 255 | 30 256 | 30 257 | 521 258 | 301 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | Scene Switcher Enabled 268 | 269 | 270 | 271 | 272 | 273 | 274 | Switch to In Game Scene when loading into game 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | In Game: 288 | 289 | 290 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | Out of Game: 307 | 308 | 309 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | Qt::RightToLeft 324 | 325 | 326 | Replay: 327 | 328 | 329 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | Qt::RightToLeft 344 | 345 | 346 | <html><head/><body><p align="right">Observing:</p></body></html> 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | Extra Scenes 361 | 362 | 363 | 364 | 365 | 20 366 | 10 367 | 251 368 | 351 369 | 370 | 371 | 372 | 373 | 374 | 375 | Score Screen 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | Home 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | Co-op Menu 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | Custom Games Menu 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | Replays Menu 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 310 428 | 10 429 | 241 430 | 351 431 | 432 | 433 | 434 | 435 | 436 | 437 | Profile 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | Lobby 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | Campaign Menu 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | Versus Menu 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | Collection Menu 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | Score Tracker 490 | 491 | 492 | 493 | 494 | 270 495 | 20 496 | 261 497 | 151 498 | 499 | 500 | 501 | 502 | 503 | 504 | Text Template: 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | <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> 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 50 528 | 20 529 | 191 530 | 116 531 | 532 | 533 | 534 | 535 | 536 | 537 | Score Tracker Enabled 538 | 539 | 540 | 541 | 542 | 543 | 544 | Popups Enabled 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | Name of text source: 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 380 568 | 200 569 | 141 570 | 151 571 | 572 | 573 | 574 | 575 | 576 | 577 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 578 | 579 | 580 | 581 | 582 | 583 | 20 584 | 200 585 | 321 586 | 175 587 | 588 | 589 | 590 | 591 | 5 592 | 593 | 594 | QLayout::SetFixedSize 595 | 596 | 597 | 598 | 599 | 600 | 601 | Qt::Vertical 602 | 603 | 604 | QSizePolicy::Fixed 605 | 606 | 607 | 608 | 20 609 | 35 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | Terran 618 | 619 | 620 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 621 | 622 | 623 | 624 | 625 | 626 | 627 | Zerg 628 | 629 | 630 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 631 | 632 | 633 | 634 | 635 | 636 | 637 | Protoss 638 | 639 | 640 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 15 650 | 651 | 652 | 653 | 654 | <html><head/><body><p align="center">Win</p></body></html> 655 | 656 | 657 | 658 | 659 | 660 | 661 | 5 662 | 663 | 664 | 665 | 666 | 667 | 55 668 | 55 669 | 670 | 671 | 672 | + 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 55 681 | 55 682 | 683 | 684 | 685 | - 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 5 695 | 696 | 697 | 698 | 699 | 700 | 55 701 | 55 702 | 703 | 704 | 705 | + 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 55 714 | 55 715 | 716 | 717 | 718 | - 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 5 728 | 729 | 730 | 731 | 732 | 733 | 55 734 | 55 735 | 736 | 737 | 738 | + 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 55 747 | 55 748 | 749 | 750 | 751 | - 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 15 763 | 764 | 765 | 766 | 767 | <html><head/><body><p align="center">Loss</p></body></html> 768 | 769 | 770 | 771 | 772 | 773 | 774 | 5 775 | 776 | 777 | 778 | 779 | 780 | 55 781 | 55 782 | 783 | 784 | 785 | + 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 55 794 | 55 795 | 796 | 797 | 798 | - 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 5 808 | 809 | 810 | 811 | 812 | 813 | 55 814 | 55 815 | 816 | 817 | 818 | + 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 55 827 | 55 828 | 829 | 830 | 831 | - 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 5 841 | 842 | 843 | 844 | 845 | 846 | 55 847 | 55 848 | 849 | 850 | 851 | + 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 55 860 | 55 861 | 862 | 863 | 864 | - 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | Game Webhook 878 | 879 | 880 | 881 | 882 | 70 883 | 30 884 | 191 885 | 22 886 | 887 | 888 | 889 | Webhook Enabled 890 | 891 | 892 | 893 | 894 | 895 | 70 896 | 60 897 | 431 898 | 41 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | URL 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 70 926 | 150 927 | 431 928 | 221 929 | 930 | 931 | 932 | 933 | 934 | 935 | URLs 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 140 948 | 110 949 | 271 950 | 36 951 | 952 | 953 | 954 | 955 | 956 | 957 | Add URL 958 | 959 | 960 | 961 | 962 | 963 | 964 | Remove URL 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | --------------------------------------------------------------------------------