diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..4120cad
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "CSSense/BluetoothLEController"]
+ path = CSSense/BluetoothLEController
+ url = git@github.com:Acurisu/BluetoothLEController.git
+[submodule "CSSense/Gist"]
+ path = CSSense/Gist
+ url = https://gist.github.com/Acurisu/1805762f2c5e2ca6508c3c2dad4f3d6b
diff --git a/CSSense.sln b/CSSense.sln
new file mode 100644
index 0000000..089141e
--- /dev/null
+++ b/CSSense.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30711.63
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CSSense", "CSSense\CSSense.vcxproj", "{A6E48085-6124-4A7B-9F3B-CB58FE4676C4}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A6E48085-6124-4A7B-9F3B-CB58FE4676C4}.Debug|x64.ActiveCfg = Debug|x64
+ {A6E48085-6124-4A7B-9F3B-CB58FE4676C4}.Debug|x64.Build.0 = Debug|x64
+ {A6E48085-6124-4A7B-9F3B-CB58FE4676C4}.Release|x64.ActiveCfg = Release|x64
+ {A6E48085-6124-4A7B-9F3B-CB58FE4676C4}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1B4C55B9-B5BB-4534-A4C8-CE0E843001F0}
+ EndGlobalSection
+EndGlobal
diff --git a/CSSense/BluetoothLEController b/CSSense/BluetoothLEController
new file mode 160000
index 0000000..53c6578
--- /dev/null
+++ b/CSSense/BluetoothLEController
@@ -0,0 +1 @@
+Subproject commit 53c6578221b405edb8113a13deb4be6342143910
diff --git a/CSSense/CSSense.cpp b/CSSense/CSSense.cpp
new file mode 100644
index 0000000..0f9c1fe
--- /dev/null
+++ b/CSSense/CSSense.cpp
@@ -0,0 +1,29 @@
+#include "pch.hpp"
+
+#include "GameStateIntegration.hpp"
+
+int main(int argc, char** argv)
+{
+ try
+ {
+ TCLAP::CmdLine cmd("CSSense CLI allows you to experience CSGO like never before", ' ', "0.1");
+
+ TCLAP::SwitchArg initSwitch("i", "init", "Initialize CSSense", cmd, false);
+
+ cmd.parse(argc, argv);
+
+ GameStateIntegration gSI(initSwitch.getValue());
+
+ gSI.open().wait();
+
+ std::cout << "Press ENTER to exit." << std::endl;
+
+ std::string line;
+ std::getline(std::cin, line);
+ gSI.close().wait();
+ }
+ catch (TCLAP::ArgException& e)
+ {
+ std::cerr << e.error() << " for arg " << e.argId() << std::endl;
+ }
+}
\ No newline at end of file
diff --git a/CSSense/CSSense.vcxproj b/CSSense/CSSense.vcxproj
new file mode 100644
index 0000000..b7bddce
--- /dev/null
+++ b/CSSense/CSSense.vcxproj
@@ -0,0 +1,191 @@
+
+
+
+
+ Debug
+ Win32
+
+
+ Release
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ {a6e48085-6124-4a7b-9f3b-cb58fe4676c4}
+ CSSense
+ 10.0.18362.0
+
+
+
+ Application
+ true
+ v142
+ Unicode
+
+
+ Application
+ false
+ v142
+ true
+ Unicode
+
+
+ Application
+ true
+ v142
+ Unicode
+
+
+ Application
+ false
+ v142
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+ Level3
+ true
+ WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ stdcpplatest
+ Use
+ pch.hpp
+ /await %(AdditionalOptions)
+
+
+ Console
+ true
+ WindowsApp.lib;bcrypt.lib;winhttp.lib;crypt32.lib;%(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ true
+ true
+ WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ stdcpplatest
+ Use
+ pch.hpp
+ /await %(AdditionalOptions)
+ MultiThreaded
+ true
+
+
+ Console
+ true
+ true
+ true
+ WindowsApp.lib;bcrypt.lib;winhttp.lib;crypt32.lib;%(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ stdcpplatest
+ Use
+ pch.hpp
+ /await %(AdditionalOptions)
+
+
+ Console
+ true
+ WindowsApp.lib;%(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ stdcpplatest
+ Use
+ pch.hpp
+ /await %(AdditionalOptions)
+ MultiThreaded
+ true
+
+
+ Console
+ true
+ true
+ true
+ WindowsApp.lib;%(AdditionalDependencies)
+
+
+
+
+ NotUsing
+ NotUsing
+
+
+
+
+ Create
+ Create
+ Create
+ Create
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CSSense/CSSense.vcxproj.filters b/CSSense/CSSense.vcxproj.filters
new file mode 100644
index 0000000..901ee15
--- /dev/null
+++ b/CSSense/CSSense.vcxproj.filters
@@ -0,0 +1,48 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/CSSense/GameStateIntegration.cpp b/CSSense/GameStateIntegration.cpp
new file mode 100644
index 0000000..58f42e6
--- /dev/null
+++ b/CSSense/GameStateIntegration.cpp
@@ -0,0 +1,619 @@
+#include "pch.hpp"
+
+#include "GameStateIntegration.hpp"
+
+void GameStateIntegration::sendVibrate(int strength)
+{
+ std::wstring cmd = L"Vibrate:" + std::to_wstring(strength) + L";";
+
+#ifndef NO_BLE
+ if (!bLEController.GattWrite(cmd))
+ {
+ std::cerr << "Failed sending a command to the device" << std::endl;
+ }
+#endif
+
+#ifdef _DEBUG
+ std::wcout << DBG_START << L"Sent: " << cmd << DBG_END << std::endl;
+#endif
+}
+
+void GameStateIntegration::handleJSON(web::json::value msg)
+{
+ if (msg.has_object_field(L"previously") && msg.has_object_field(L"player"))
+ {
+ web::json::value player = msg.at(L"player");
+ web::json::value previously = msg.at(L"previously");
+
+ if (previously.has_object_field(L"player"))
+ {
+ web::json::value previousPlayer = previously.at(L"player");
+
+ if (previously.has_boolean_field(L"map"))
+ {
+ if (previously.at(L"map").as_bool())
+ {
+#ifdef EVENTS
+ std::cout << EVT("Map unload") << std::endl;
+#endif
+
+ currentStrength = config.baseVibration;
+ isMVP = false;
+ isDead = false;
+ sendVibrate(config.baseVibration);
+ return;
+ }
+ }
+
+ if (previously.has_object_field(L"round"))
+ {
+ web::json::value round = previously.at(L"round");
+ if (round.has_string_field(L"phase") && round.at(L"phase").as_string() == L"over")
+ {
+#ifdef EVENTS
+ std::cout << EVT("Round reset") << std::endl;
+#endif
+
+ currentStrength = config.baseVibration;
+ isMVP = false;
+ isDead = false;
+ sendVibrate(config.baseVibration);
+ return;
+ }
+ }
+
+ if (isDead || (isMVP && config.burstOnMVP))
+ {
+ return;
+ }
+
+ if (previousPlayer.has_object_field(L"match_stats"))
+ {
+ web::json::value match_stats = previousPlayer.at(L"match_stats");
+ if (config.burstOnMVP)
+ {
+ if (match_stats.has_integer_field(L"mvps"))
+ {
+#ifdef EVENTS
+ std::cout << EVT("MVP") << std::endl;
+#endif
+
+ isMVP = true;
+ sendVibrate(20);
+ return;
+ }
+ }
+
+ if (match_stats.has_integer_field(L"deaths"))
+ {
+#ifdef EVENTS
+ std::cout << EVT("Dead") << std::endl;
+#endif
+
+ isDead = true;
+ currentStrength = config.baseVibration;
+ sendVibrate(config.baseVibration);
+ return;
+ }
+ }
+
+ if (config.increaseOnKill)
+ {
+ if (previousPlayer.has_object_field(L"state") && player.has_object_field(L"state"))
+ {
+ web::json::value state = player.at(L"state");
+
+ if (previousPlayer.at(L"state").has_integer_field(L"round_kills") && state.has_integer_field(L"round_kills"))
+ {
+#ifdef EVENTS
+ std::cout << EVT("Kill") << std::endl;
+#endif
+
+ web::json::value round_kills = state.at(L"round_kills");
+ int strength = static_cast(config.baseVibration + round_kills.as_integer() * config.increaseAmount);
+ currentStrength = strength;
+ sendVibrate(std::clamp(strength, 0, 20));
+ }
+ }
+ }
+
+ if (config.stopOnKnife || config.vibrateOnShoot)
+ {
+ if (previousPlayer.has_object_field(L"weapons") && player.has_object_field(L"weapons"))
+ {
+ web::json::value previousWeapons = previousPlayer.at(L"weapons");
+ web::json::object weapons = player.at(L"weapons").as_object();
+ for (auto it = weapons.begin(); it != weapons.end(); ++it)
+ {
+ std::wstring name = it->first;
+ web::json::value weapon = it->second;
+
+ if (weapon.has_string_field(L"state") && weapon.at(L"state").as_string() == L"active")
+ {
+ if (previousWeapons.has_object_field(name))
+ {
+ web::json::value previousWeapon = previousWeapons.at(name);
+
+ if (config.vibrateOnShoot)
+ {
+ if (previousWeapon.has_integer_field(L"ammo_clip") && weapon.has_integer_field(L"ammo_clip") &&
+ !previousWeapon.has_string_field(L"name") &&
+ previousWeapon.at(L"ammo_clip").as_integer() - 1 == weapon.at(L"ammo_clip").as_integer())
+ {
+#ifdef EVENTS
+ std::cout << EVT("Shot") << std::endl;
+#endif
+
+ lastShot = std::chrono::steady_clock::now();
+ sendVibrate(currentStrength);
+
+ pplx::wait(100);
+
+ auto timeElapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - lastShot).count();
+ if (timeElapsed > 100 && !isMVP && config.vibrateOnShoot)
+ {
+#ifdef _DEBUG
+ std::cout << DBG("Reset") << std::endl;
+#endif
+
+ sendVibrate(config.baseVibration);
+ }
+ }
+ }
+ else if (config.stopOnKnife)
+ {
+ if (previousWeapon.has_string_field(L"state") && previousWeapon.at(L"state").as_string() == L"holstered")
+ {
+ if (weapon.has_string_field(L"type") && weapon.at(L"type").as_string() == L"Knife")
+ {
+#ifdef EVENTS
+ std::cout << EVT("Knife active") << std::endl;
+#endif
+
+ sendVibrate(config.baseVibration);
+ return;
+ }
+#ifdef EVENTS
+ std::cout << EVT("Knife holstered") << std::endl;
+#endif
+
+ sendVibrate(currentStrength);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+void GameStateIntegration::handlePost(http_request message)
+{
+#ifdef _DEBUG
+ std::cout << DBG("Received post") << std::endl;
+#endif
+
+ message.reply(status_codes::OK);
+
+ web::json::value msg = message.extract_json().get();
+
+ handleJSON(msg);
+}
+
+void GameStateIntegration::setupListener(int port)
+{
+ mListener = http_listener(uri_builder(L"http://localhost:" + std::to_wstring(port)).to_string());
+ mListener.support(methods::POST, std::bind(&GameStateIntegration::handlePost, this, std::placeholders::_1));
+}
+
+std::optional GameStateIntegration::getCSGOPath()
+{
+ winreg::RegKey key{ HKEY_CURRENT_USER, L"SOFTWARE\\Valve\\Steam" };
+ if (std::optional steamPathO = key.TryGetStringValue(L"SteamPath"))
+ {
+ std::wstring steamPath = *steamPathO + std::wstring(L"/config/config.vdf"); // using steamapps/libraryfolders.vdf would be possible too
+
+#ifdef _DEBUG
+ std::wcout << DBG_START << L"Trying to open steam config at: " << steamPath << DBG_END << std::endl;
+#endif
+
+ std::ifstream steamConfig;
+ steamConfig.open(steamPath);
+
+ if (steamConfig.good())
+ {
+ std::regex baseInstallRegex("\"BaseInstallFolder_[0-9]+\"\t+\"(.+)\"");
+
+ std::string line;
+ while (getline(steamConfig, line))
+ {
+ std::smatch matches;
+
+ if (std::regex_search(line, matches, baseInstallRegex))
+ {
+#ifdef _DEBUG
+ std::cout << DBG_START << "Found install folder at: " << matches[1] << DBG_END << std::endl;
+#endif
+
+ std::string csgoPath = std::string(matches[1]) + "\\\\steamapps\\\\common\\\\Counter-Strike Global Offensive\\\\csgo\\\\cfg";
+ DWORD fileAttributes = GetFileAttributesA(csgoPath.c_str());
+
+ if (fileAttributes != INVALID_FILE_ATTRIBUTES && fileAttributes & FILE_ATTRIBUTE_DIRECTORY)
+ {
+ return csgoPath;
+ }
+ }
+ }
+ }
+ else
+ {
+#ifdef _DEBUG
+ std::cout << DBG("Could not read config.vdf") << std::endl;
+#endif
+
+ return {};
+ }
+ }
+
+ return {};
+}
+
+int GameStateIntegration::createConfig()
+{
+ int port;
+ std::string input;
+ std::cout << "Enter the port to be used (default: 6969): ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+
+ if (!input.empty())
+ {
+ std::istringstream stream(input);
+ stream >> port;
+
+ if (port == 0)
+ {
+ port = 6969;
+ }
+
+ if (port < 1025)
+ {
+ std::cout << "You entered a well-known port. This potentially requires admin permissions." << std::endl;
+ }
+ }
+ else
+ {
+ port = 6969;
+ }
+
+#ifdef _DEBUG
+ std::cout << DBG_START << "Selected port is: " << port << DBG_END << std::endl;
+#endif
+
+ std::string csgoPath;
+ if (std::optional csgoPathO = getCSGOPath())
+ {
+ csgoPath = *csgoPathO;
+
+#ifdef _DEBUG
+ std::cout << DBG_START << "Found CSGO directory at: " << csgoPath << DBG_END << std::endl;
+#endif
+
+ }
+ else
+ {
+ std::cout << "Could not determine CSGO's path automatically.\nPlease enter its installation location: ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, csgoPath);
+
+ while (true)
+ {
+ csgoPath = std::regex_replace(csgoPath, std::regex("\\\\"), "\\\\");
+ csgoPath += std::string("\\\\csgo\\\\cfg");
+ DWORD fileAttributes = GetFileAttributesA(csgoPath.c_str());
+
+ if (fileAttributes != INVALID_FILE_ATTRIBUTES && fileAttributes & FILE_ATTRIBUTE_DIRECTORY) // fix
+ {
+ break;
+ }
+
+ std::cerr << "The path you have entered is not valid\n";
+ std::cout << "Please enter a valid csgo path: ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, csgoPath);
+ }
+ }
+
+ std::string gameStateConfig(R"("CSSense"
+{
+ "uri" "http://localhost:)" + std::to_string(port) + R"("
+ "timeout" "12.0"
+ "buffer" "0.0"
+ "throttle" "0.0"
+ "heartbeat" "120"
+ "output"
+ {
+ "precision_time" "3"
+ "precision_position" "1"
+ "precision_vector" "3"
+ }
+ "data"
+ {
+ "provider" "1"
+ "map" "1"
+ "round" "1"
+ "player_id" "1"
+ "player_state" "1"
+ "player_weapons" "1"
+ "player_match_stats" "1"
+ }
+})");
+
+#ifdef _DEBUG
+ std::cout << DBG_START << "Writing gamestate file to: " << csgoPath << DBG_END << std::endl;
+#endif
+
+ std::ofstream gameStateFile(csgoPath + "\\gamestate_integration_cssense.cfg");
+ gameStateFile << gameStateConfig;
+ gameStateFile.close();
+
+ std::cout << "\nNow let's set up the config.\nEnter the minimum amount of strength the device should vibrate at at all times (0-20, integer) (default: 0): ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+ if (!input.empty())
+ {
+ std::istringstream stream(input);
+ stream >> config.baseVibration;
+
+ config.baseVibration = std::clamp(0, 20, config.baseVibration);
+ }
+ else
+ {
+ config.baseVibration = 0;
+ }
+
+ std::cout << "Should the strength increase after a kill (T/F) (default: T)? ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+ if (!input.empty() && (input.starts_with('f') || input.starts_with('F')))
+ {
+ config.increaseOnKill = false;
+ config.increaseAmount = 0;
+ config.vibrateOnShoot = false;
+ config.stopOnKnife = false;
+ }
+ else
+ {
+ config.increaseOnKill = true;
+
+ std::cout << "Enter the amount by which the strength should get increased (or decreased) on kill (-20-20, double) (default: 3): ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+ if (!input.empty())
+ {
+ std::istringstream stream(input);
+ stream >> config.increaseAmount;
+ config.increaseAmount = std::clamp(config.increaseAmount, -20., 20.);
+ }
+ else
+ {
+ config.increaseAmount = 3;
+ }
+
+ std::cout << "Should the increased strength only get applied when shooting (T/F) (default: T)? ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+ if (!input.empty() && (input.starts_with('f') || input.starts_with('F')))
+ {
+ config.vibrateOnShoot = false;
+
+ std::cout << "Should the device return to base vibration when holding a knife (T/F) (default: T)? ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+ if (!input.empty() && (input.starts_with('f') || input.starts_with('F')))
+ {
+ config.stopOnKnife = false;
+ }
+ else
+ {
+ config.stopOnKnife = true;
+ }
+ }
+ else
+ {
+ config.vibrateOnShoot = true;
+ config.stopOnKnife = false;
+ }
+ }
+
+ std::cout << "Should the device vibrate at maximum strength on MVP (T/F) (default: T)? ";
+ std::cin.clear();
+ std::cin.sync();
+ std::getline(std::cin, input);
+ if (!input.empty() && (input.starts_with('f') || input.starts_with('F')))
+ {
+ config.burstOnMVP = false;
+ }
+ else
+ {
+ config.burstOnMVP = true;
+ }
+
+ std::ostringstream configStream;
+ configStream << std::boolalpha << "{\n\t\"baseVibration\": " << config.baseVibration <<
+ ",\n\t\"increaseOnKill\": " << config.increaseOnKill <<
+ ",\n\t\"increaseAmount\": " << config.increaseAmount <<
+ ",\n\t\"vibrateOnShoot\": " << config.vibrateOnShoot <<
+ ",\n\t\"stopOnKnife\": " << config.stopOnKnife <<
+ ",\n\t\"burstOnMVP\": " << config.burstOnMVP <<
+ ",\n\t\"gamestateConfigPath\": \"" << csgoPath << "\"\n}";
+
+#ifdef _DEBUG
+ std::cout << DBG("Writing default config") << std::endl;
+
+ std::cout << DBG_START << configStream.str() << DBG_END << std::endl;
+#endif
+
+ std::ofstream configFile("config.json");
+ configFile << configStream.str();
+ configFile.close();
+
+ std::cout << "\nEverything has been initialized.\n"
+ "You can configure the config to your liking by either editing 'config.json' or by running 'CSSense -i' again.\n"
+ "You can find an explanation on https://github.com/Acurisu/CSSense-CLI.\n"
+ "Restart CSGO if you had it running.\nEnjoy =^w^=\n" << std::endl;
+
+ return port;
+}
+
+int GameStateIntegration::parseConfig()
+{
+ std::ifstream configFile;
+ configFile.open("config.json");
+
+ if (configFile.good())
+ {
+ std::stringstream buffer;
+ buffer << configFile.rdbuf();
+
+ try
+ {
+ web::json::value cfg = web::json::value::parse(buffer);
+ config.increaseOnKill = cfg.at(L"increaseOnKill").as_bool();
+ config.vibrateOnShoot = cfg.at(L"vibrateOnShoot").as_bool();
+ config.stopOnKnife = cfg.at(L"stopOnKnife").as_bool();
+ config.burstOnMVP = cfg.at(L"burstOnMVP").as_bool();
+ config.increaseAmount = cfg.at(L"increaseAmount").as_number().to_double();
+ config.baseVibration = cfg.at(L"baseVibration").as_integer();
+
+ std::wstring gamestateConfigPath = cfg.at(L"gamestateConfigPath").as_string();
+
+#ifdef _DEBUG
+ std::wcout << std::boolalpha << DBG_START << L"baseVibration:\t" << config.baseVibration
+ << L"\tincreaseOnKill:\t" << config.increaseOnKill
+
+ << L"\n[DBG] increaseAmount:\t" << config.increaseAmount
+ << L"\tvibrateOnShoot:\t" << config.vibrateOnShoot
+
+ << L"\n[DBG] stopOnKnife:\t" << config.stopOnKnife
+ << L"\tburstOnMVP:\t" << config.burstOnMVP
+
+ << L"\n[DBG] gamestateConfigPath:\t" << gamestateConfigPath << DBG_END << std::endl;
+#endif
+
+ std::ifstream gamestateFile;
+ gamestateFile.open(gamestateConfigPath + L"\\gamestate_integration_cssense.cfg");
+
+ if (gamestateFile.good())
+ {
+ std::regex portRegex("http:\\/\\/localhost:([0-9]+)");
+
+ std::string line;
+ while (std::getline(gamestateFile, line))
+ {
+ std::smatch matches;
+
+ if (std::regex_search(line, matches, portRegex))
+ {
+#ifdef _DEBUG
+ std::cout << DBG_START << "Found port: " << matches[1] << DBG_END << std::endl;
+#endif
+
+ return std::stoi(matches[1]);
+ }
+ }
+ }
+ else
+ {
+ std::cerr << "Something went wrong trying to retrieve the port.\n"
+ "Please run 'CSSense -i' again." << std::endl;
+ }
+ }
+ catch (web::json::json_exception e)
+ {
+ std::cerr << "Something went wrong trying to parse the config.\n"
+ "Please run 'CSSense -i' again or fix the error shown below:\n"
+ << e.what() << std::endl;
+ }
+ }
+ else
+ {
+ std::cerr << "Something went wrong trying to read the config.\nDid you forget to run 'CSSense -i'?" << std::endl;
+ }
+
+ return -1;
+}
+
+GameStateIntegration::GameStateIntegration(bool init)
+{
+ int port;
+ if (init)
+ {
+ port = createConfig();
+ }
+ else
+ {
+ port = parseConfig();
+ }
+
+ if (port == -1)
+ {
+ exit(-1);
+ }
+
+#ifndef NO_BLE
+ std::wregex serviceGUIDRegex(L"^\\{..300001-002.-4bd4-bbd5-a6920e4c5653\\}");
+
+ std::cout << "Searching for a Lovesense device..." << std::endl;
+
+ if (!bLEController.ConnectBLEDeviceByService(serviceGUIDRegex))
+ {
+ std::cerr << "Could not connect to the BLE device" << std::endl;
+ exit(-1);
+ }
+
+ if (!bLEController.SelectCharacteristics([](
+ winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::
+ GattCharacteristic sender,
+ winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::
+ GattValueChangedEventArgs eventArgs)
+ {
+#ifdef _DEBUG
+ std::cout << DBG_START << "Received: " << eventArgs.CharacteristicValue().data() << DBG_END << std::endl;
+#endif
+ }))
+ {
+ std::cerr << "Could not select the specified characteristics" << std::endl;
+ exit(-1);
+ }
+
+ std::cout << "Found and connected to the device." << std::endl;
+#endif
+
+ currentStrength = config.baseVibration;
+ sendVibrate(config.baseVibration);
+ setupListener(port);
+}
+
+GameStateIntegration::~GameStateIntegration()
+{
+ sendVibrate(0);
+
+#ifndef NO_BLE
+ if (!bLEController.DisconnectBLEDevice())
+ {
+ std::cerr << "Failed trying to disconnect the BLE device" << std::endl;
+ exit(-1);
+ }
+#endif
+}
\ No newline at end of file
diff --git a/CSSense/GameStateIntegration.hpp b/CSSense/GameStateIntegration.hpp
new file mode 100644
index 0000000..cf3030e
--- /dev/null
+++ b/CSSense/GameStateIntegration.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "Macros.h"
+
+#ifdef _DEBUG
+#define NO_BLE
+#define EVENTS
+#endif
+
+using namespace web;
+using namespace http;
+using namespace http::experimental::listener;
+
+struct Config
+{
+ int baseVibration;
+ bool increaseOnKill;
+ double increaseAmount;
+ bool vibrateOnShoot;
+ bool stopOnKnife;
+ bool burstOnMVP;
+};
+
+class GameStateIntegration
+{
+private:
+ http_listener mListener;
+ Config config;
+ BluetoothLEController bLEController;
+ int currentStrength;
+ bool isMVP = false;
+ bool isDead = false;
+ std::chrono::steady_clock::time_point lastShot;
+
+ void sendVibrate(int strength);
+ void handleJSON(web::json::value msg);
+ void handlePost(http_request message);
+ void setupListener(int port);
+ std::optional getCSGOPath();
+ int createConfig();
+ int parseConfig();
+
+public:
+ GameStateIntegration(bool init);
+ ~GameStateIntegration();
+
+ pplx::task open() { return mListener.open(); }
+ pplx::task close() { return mListener.close(); }
+};
+
diff --git a/CSSense/Gist b/CSSense/Gist
new file mode 160000
index 0000000..0d62d27
--- /dev/null
+++ b/CSSense/Gist
@@ -0,0 +1 @@
+Subproject commit 0d62d27fa1d1b3c84e7567d1e0b98a24cdfb4e59
diff --git a/CSSense/Macros.h b/CSSense/Macros.h
new file mode 100644
index 0000000..fab34df
--- /dev/null
+++ b/CSSense/Macros.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#define DBG(txt) COLORIZE(FA_DEFAULT, FG_GRAY, BG_DEFAULT, "[DBG] " txt, RST_ALL)
+#define DBG_START COLOR_START(FA_DEFAULT, FG_GRAY, BG_DEFAULT) "[DBG] "
+#define DBG_END COLOR_END(RST_ALL)
+
+#define EVT(txt) COLORIZE(FA_DEFAULT, FG_SET24(42, 90, 90), BG_DEFAULT, "[EVT] " txt, RST_ALL)
\ No newline at end of file
diff --git a/CSSense/pch.cpp b/CSSense/pch.cpp
new file mode 100644
index 0000000..3854579
--- /dev/null
+++ b/CSSense/pch.cpp
@@ -0,0 +1 @@
+#include "pch.hpp"
diff --git a/CSSense/pch.hpp b/CSSense/pch.hpp
new file mode 100644
index 0000000..33eef3e
--- /dev/null
+++ b/CSSense/pch.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#define WIN32_LEAN_AND_MEAN
+#include
+#include
+
+#include "BluetoothLEController/BluetoothLEController/BluetoothLEController.hpp"
+#include "Gist/Color.h"
\ No newline at end of file