├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── .name ├── inspectionProfiles │ └── Project_Default.xml ├── libsteam.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── CMakeLists.txt ├── README.md ├── lib ├── CMakeLists.txt ├── include │ ├── steam.hpp │ └── steam │ │ ├── api │ │ ├── appid.hpp │ │ ├── steam_api.hpp │ │ ├── steam_grid_db.hpp │ │ └── user.hpp │ │ ├── file_formats │ │ ├── keyvalues.hpp │ │ └── vdf.hpp │ │ └── helpers │ │ ├── file.hpp │ │ ├── fs.hpp │ │ ├── hash.hpp │ │ ├── net.hpp │ │ └── utils.hpp └── source │ ├── api │ ├── steam_api.cpp │ └── steam_grid_api.cpp │ ├── file_formats │ ├── keyvalues.cpp │ └── vdf.cpp │ └── helpers │ ├── file.cpp │ ├── net.cpp │ └── utils.cpp └── test ├── CMakeLists.txt └── source └── main.cpp /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | BUILD_TYPE: Release 9 | 10 | jobs: 11 | 12 | linux: 13 | runs-on: ubuntu-20.04 14 | name: 🐧 Ubuntu 20.04 15 | steps: 16 | 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | submodules: recursive 22 | 23 | - name: Install deps 24 | run: | 25 | sudo apt update 26 | sudo apt install -y gcc-10 g++-10 cmake make build-essential libcurl4-gnutls-dev 27 | 28 | - name: Build 29 | run: | 30 | mkdir -p build 31 | cd build 32 | CC=gcc-10 CXX=g++-10 cmake -DCMAKE_INSTALL_PREFIX="$PWD/install" .. 33 | make -j4 install 34 | 35 | - name: Upload 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: ELF 39 | path: | 40 | build/install/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-debug/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/fmt"] 2 | path = external/fmt 3 | url = https://github.com/fmtlib/fmt 4 | [submodule "https://github.com/nlohmann/json"] 5 | path = https://github.com/nlohmann/json 6 | url = external/json 7 | [submodule "external/json"] 8 | path = external/json 9 | url = https://github.com/nlohmann/json 10 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | main -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | -------------------------------------------------------------------------------- /.idea/libsteam.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(main) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_SHARED_LIBRARY_PREFIX "") 6 | 7 | find_package(PkgConfig REQUIRED) 8 | pkg_check_modules(LIBCURL REQUIRED IMPORTED_TARGET libcurl) 9 | 10 | add_subdirectory(external/fmt) 11 | set(JSON_MultipleHeaders ON CACHE BOOL "Enable Multiple Headers" FORCE) 12 | add_subdirectory(external/json) 13 | unset(JSON_MultipleHeaders) 14 | set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) 15 | set_target_properties(nlohmann_json PROPERTIES POSITION_INDEPENDENT_CODE ON) 16 | 17 | add_subdirectory(lib) 18 | add_subdirectory(test) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libsteam 2 | 3 | A nice and easy to use C++ library to interact with Steam and its file formats. Mainly intended for the Steam Deck but it will work elsewhere too 4 | 5 | ## Features 6 | - Binary VDF File parser (e.g shortcuts.vdf) 7 | - Parsing 8 | - Modifying fields 9 | - Adding new fields 10 | - Dumping back to binary representation 11 | - Pretty printing 12 | - KeyValue File parser (e.g config.vdf) 13 | - Parsing 14 | - Modifying fields 15 | - Adding new fields 16 | - Dumping back to text representation 17 | - Pretty printing 18 | - Interaction with the Steam Game UI 19 | - Restarting Game UI 20 | - Adding new shortcuts to Steam 21 | - Removing shortcuts from Steam 22 | - Enabling Proton for shortcuts 23 | - Querying the SteamGridDB API 24 | - Searching 25 | - Getting Grids, Heroes, Logos and Icons 26 | 27 | ## Example 28 | 29 | ### shortcut.vdf interaction 30 | ```cpp 31 | 32 | std::vector vdfFileContent = /* ... */; 33 | 34 | steam::VDF vdf(vdfFileContent); 35 | 36 | // Print game name of first entry 37 | auto firstGameName = vdf["shortcuts"]["0"]["AppName"].string(); 38 | std::printf("%s", firstGameName.c_str()); 39 | 40 | // Hide first game from UI 41 | vdf["shortcuts"]["0"]["IsHidden"] = true; 42 | 43 | // Convert back to binary representation 44 | auto binaryData = vdf.dump(); 45 | 46 | // Print vdf as formatted data 47 | std::printf("%s", vdf.format().c_str()); 48 | ``` 49 | 50 | Check out the [/test](/test) folder for more examples -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(libsteam) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_SHARED_LIBRARY_PREFIX "") 6 | 7 | add_library(libsteam SHARED 8 | source/api/steam_api.cpp 9 | source/api/steam_grid_api.cpp 10 | 11 | source/file_formats/vdf.cpp 12 | source/file_formats/keyvalues.cpp 13 | 14 | source/helpers/file.cpp 15 | source/helpers/utils.cpp 16 | source/helpers/net.cpp 17 | ) 18 | 19 | set_target_properties(libsteam 20 | PROPERTIES 21 | ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" 22 | LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" 23 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" 24 | ) 25 | 26 | target_link_libraries(libsteam PUBLIC fmt::fmt nlohmann_json curl) 27 | 28 | target_include_directories(libsteam PUBLIC include) 29 | 30 | install(TARGETS libsteam) -------------------------------------------------------------------------------- /lib/include/steam.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace steam { 6 | 7 | using u8 = std::uint8_t; 8 | using u16 = std::uint16_t; 9 | using u32 = std::uint32_t; 10 | using u64 = std::uint64_t; 11 | using u128 = __uint128_t; 12 | 13 | using i8 = std::int8_t; 14 | using i16 = std::int16_t; 15 | using i32 = std::int32_t; 16 | using i64 = std::int64_t; 17 | using i128 = __int128_t; 18 | 19 | using f32 = float; 20 | using f64 = double; 21 | using f128 = long double; 22 | } -------------------------------------------------------------------------------- /lib/include/steam/api/appid.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace steam::api { 14 | 15 | class AppId { 16 | public: 17 | AppId() : m_appId(-1) { } 18 | 19 | AppId(const std::fs::path &exePath, const std::string &appName) noexcept 20 | : m_appId((u64(crc32(exePath.string() + appName) | 0x8000'0000) << 32) | 0x0200'0000) { } 21 | 22 | explicit AppId(u64 appId) : m_appId(appId) { } 23 | 24 | static std::vector getAppIds(const api::User &user) { 25 | auto configPath = fs::getSteamDirectory() / "userdata" / std::to_string(user.getId()) / "config"; 26 | 27 | // Open shortcuts file 28 | auto shortcutsFile = fs::File(configPath / "shortcuts.vdf", fs::File::Mode::Write); 29 | if (!shortcutsFile.isValid()) { 30 | return { }; 31 | } 32 | 33 | // Parse shortcuts 34 | auto shortcuts = VDF(shortcutsFile.readBytes()); 35 | 36 | // Query all app IDs from the list 37 | std::vector appIds; 38 | for (const auto &[key, value] : shortcuts["shortcuts"].set()) { 39 | if (value.set().contains("appid")) 40 | appIds.emplace_back(value.set().at("appid").integer()); 41 | } 42 | 43 | return appIds; 44 | } 45 | 46 | [[nodiscard]] 47 | u64 getAppId() const noexcept { 48 | return this->m_appId; 49 | } 50 | 51 | [[nodiscard]] 52 | u32 getShortAppId() const noexcept { 53 | return this->m_appId >> 32; 54 | } 55 | 56 | [[nodiscard]] 57 | u32 getShortcutId() const noexcept { 58 | return (this->m_appId >> 32) - 0x1'0000'0000; 59 | } 60 | 61 | [[nodiscard]] 62 | explicit operator u64() const noexcept { 63 | return this->m_appId; 64 | } 65 | 66 | private: 67 | u64 m_appId; 68 | }; 69 | 70 | } -------------------------------------------------------------------------------- /lib/include/steam/api/steam_api.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | namespace steam::api { 13 | 14 | std::optional addGameShortcut(const User &user, const std::string &appName, const std::fs::path &exePath, const std::string &launchOptions = "", const std::vector &tags = { }, bool hidden = false); 15 | bool removeGameShortcut(const User &user, const AppId &appId); 16 | bool enableProtonForApp(AppId appId, bool enabled); 17 | 18 | bool restartSteam(); 19 | } -------------------------------------------------------------------------------- /lib/include/steam/api/steam_grid_db.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace steam::api { 8 | 9 | class SteamGridDBAPI { 10 | public: 11 | SteamGridDBAPI(const std::string &apiKey) : m_apiKey(apiKey) { } 12 | 13 | const static inline std::string BaseUrl = "https://www.steamgriddb.com/api/v2"; 14 | 15 | struct SearchEntry { 16 | std::vector types; 17 | AppId id; 18 | std::string name; 19 | bool verified; 20 | }; 21 | 22 | struct ImageResult { 23 | AppId id; 24 | u32 score; 25 | std::string style; 26 | std::string url; 27 | std::string thumb; 28 | }; 29 | 30 | std::future> search(const std::string &keyword); 31 | 32 | std::future> getGrids(const AppId &appId); 33 | std::future> getHeroes(const AppId &appId); 34 | std::future> getLogos(const AppId &appId); 35 | std::future> getIcons(const AppId &appId); 36 | 37 | private: 38 | std::string m_apiKey; 39 | Net m_net; 40 | }; 41 | 42 | } -------------------------------------------------------------------------------- /lib/include/steam/api/user.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace steam::api { 12 | 13 | class User { 14 | public: 15 | explicit User(u32 userId) : m_userId(userId), m_userName(queryUserName(userId)) { } 16 | 17 | static const std::vector& getUsers() { 18 | static auto users = []{ 19 | std::vector users; 20 | 21 | for (const auto &folder : std::fs::directory_iterator(fs::getSteamDirectory() / "userdata")) { 22 | u32 userId = std::stoi(folder.path().filename()); 23 | if (userId == 0) 24 | continue; 25 | 26 | users.emplace_back(userId); 27 | } 28 | 29 | std::erase_if(users, [](const User &user){ 30 | return user.getName().empty(); 31 | }); 32 | 33 | return users; 34 | }(); 35 | 36 | return users; 37 | } 38 | 39 | [[nodiscard]] 40 | u32 getId() const noexcept { 41 | return this->m_userId; 42 | } 43 | 44 | [[nodiscard]] 45 | const std::string &getName() const noexcept { 46 | return this->m_userName; 47 | } 48 | 49 | public: 50 | static std::string queryUserName(u32 userId) { 51 | auto localConfigFile = fs::File(fs::getSteamDirectory() / "userdata" / std::to_string(userId) / "config" / "localconfig.vdf", fs::File::Mode::Read); 52 | if (!localConfigFile.isValid()) 53 | return { }; 54 | 55 | auto localConfig = steam::KeyValues(localConfigFile.readString()); 56 | if (!localConfig.contains("UserLocalConfigStore")) return { }; 57 | 58 | auto userLocalConfigStore = localConfig["UserLocalConfigStore"]; 59 | if (!userLocalConfigStore.contains("friends")) return { }; 60 | 61 | auto friends = userLocalConfigStore["friends"]; 62 | if (!friends.contains(std::to_string(userId))) return { }; 63 | 64 | auto user = friends[std::to_string(userId)]; 65 | if (!user.contains("name")) return { }; 66 | 67 | return user["name"].string(); 68 | } 69 | 70 | u32 m_userId; 71 | std::string m_userName; 72 | }; 73 | 74 | } -------------------------------------------------------------------------------- /lib/include/steam/file_formats/keyvalues.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace steam { 14 | 15 | class KeyValues { 16 | public: 17 | 18 | KeyValues() = default; 19 | explicit KeyValues(const std::fs::path &path) : m_content(parse(fs::File(path, fs::File::Mode::Read).readString())) { } 20 | explicit KeyValues(const std::string &content) : m_content(parse(content)) { } 21 | 22 | struct Value; 23 | 24 | using Set = std::map; 25 | 26 | struct Value { 27 | std::variant content; 28 | 29 | [[nodiscard]] 30 | std::string& string() { 31 | return std::get(content); 32 | } 33 | 34 | [[nodiscard]] 35 | const KeyValues::Set& set() const { 36 | return std::get(content); 37 | } 38 | 39 | [[nodiscard]] 40 | KeyValues::Set& set() { 41 | return std::get(content); 42 | } 43 | 44 | [[nodiscard]] 45 | const std::string& string() const { 46 | return std::get(content); 47 | } 48 | 49 | [[nodiscard]] 50 | bool isString() const { 51 | return std::get_if(&content) != nullptr; 52 | } 53 | 54 | [[nodiscard]] 55 | bool isSet() const { 56 | return std::get_if(&content) != nullptr; 57 | } 58 | 59 | [[nodiscard]] 60 | Value& operator[](const std::string &key) & { 61 | return std::get(content)[key]; 62 | } 63 | 64 | [[nodiscard]] 65 | Value&& operator[](const std::string &key) && { 66 | return std::move(std::get(content)[key]); 67 | } 68 | 69 | [[nodiscard]] 70 | Value& operator[](const char *key) & { 71 | return std::get(content)[key]; 72 | } 73 | 74 | [[nodiscard]] 75 | Value&& operator[](const char *key) && { 76 | return std::move(std::get(content)[key]); 77 | } 78 | 79 | auto& operator=(const std::string &value) { 80 | this->content = value; 81 | return *this; 82 | } 83 | 84 | auto& operator=(const Set &value) { 85 | this->content = value; 86 | return *this; 87 | } 88 | 89 | [[nodiscard]] 90 | explicit operator const std::string&() const { 91 | return this->string(); 92 | } 93 | 94 | [[nodiscard]] 95 | explicit operator std::string() { 96 | return this->string(); 97 | } 98 | 99 | bool operator==(const Value &other) const { 100 | return this->content == other.content; 101 | } 102 | 103 | bool operator!=(const Value &other) const { 104 | return this->content != other.content; 105 | } 106 | 107 | [[nodiscard]] 108 | bool contains(const std::string &key) { 109 | auto set = std::get_if(&this->content); 110 | if (set == nullptr) 111 | return false; 112 | 113 | return set->contains(key); 114 | } 115 | 116 | }; 117 | 118 | struct KeyValuePair { 119 | std::string key; 120 | Value value; 121 | }; 122 | 123 | 124 | [[nodiscard]] 125 | Value& operator[](const char *key) { 126 | return this->m_content[key]; 127 | } 128 | 129 | [[nodiscard]] 130 | const Value& operator[](const char *key) const { 131 | return this->m_content.at(key); 132 | } 133 | 134 | [[nodiscard]] 135 | Value& operator[](const std::string &key) { 136 | return this->m_content[key]; 137 | } 138 | 139 | [[nodiscard]] 140 | const Value& operator[](const std::string &key) const { 141 | return this->m_content.at(key); 142 | } 143 | 144 | [[nodiscard]] 145 | Set& get() { 146 | return this->m_content; 147 | } 148 | 149 | [[nodiscard]] 150 | const Set& get() const { 151 | return this->m_content; 152 | } 153 | 154 | [[nodiscard]] 155 | std::string dump() const; 156 | 157 | [[nodiscard]] 158 | bool contains(const std::string &key) { 159 | return this->m_content.contains(key); 160 | } 161 | 162 | bool operator==(const KeyValues &other) const { 163 | return this->m_content == other.m_content; 164 | } 165 | 166 | bool operator!=(const KeyValues &other) const { 167 | return this->m_content != other.m_content; 168 | } 169 | 170 | private: 171 | Set parse(const std::string &data); 172 | Set m_content; 173 | }; 174 | 175 | } -------------------------------------------------------------------------------- /lib/include/steam/file_formats/vdf.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | namespace steam { 12 | 13 | class VDF { 14 | public: 15 | VDF() = default; 16 | explicit VDF(const std::fs::path &path) : m_content(parse(fs::File(path, fs::File::Mode::Read).readBytes())) { } 17 | explicit VDF(const std::vector &content) : m_content(parse(content)) { } 18 | 19 | enum class Type : u8 { 20 | Set = 0x00, 21 | String = 0x01, 22 | Integer = 0x02, 23 | EndSet = 0x08 24 | }; 25 | 26 | struct Value; 27 | 28 | using Set = std::map; 29 | 30 | struct Value { 31 | std::variant content; 32 | 33 | [[nodiscard]] 34 | std::string& string() { 35 | return std::get(content); 36 | } 37 | 38 | [[nodiscard]] 39 | const std::string& string() const { 40 | return std::get(content); 41 | } 42 | 43 | [[nodiscard]] 44 | u32& integer() { 45 | return std::get(content); 46 | } 47 | 48 | [[nodiscard]] 49 | const u32& integer() const { 50 | return std::get(content); 51 | } 52 | 53 | [[nodiscard]] 54 | Set& set() { 55 | return std::get(content); 56 | } 57 | 58 | [[nodiscard]] 59 | const Set& set() const { 60 | return std::get(content); 61 | } 62 | 63 | [[nodiscard]] 64 | bool isInteger() const { 65 | return std::get_if(&content) != nullptr; 66 | } 67 | 68 | [[nodiscard]] 69 | bool isString() const { 70 | return std::get_if(&content) != nullptr; 71 | } 72 | 73 | [[nodiscard]] 74 | bool isSet() const { 75 | return std::get_if(&content) != nullptr; 76 | } 77 | 78 | [[nodiscard]] 79 | Value& operator[](const std::string &key) & { 80 | return std::get(content)[key]; 81 | } 82 | 83 | [[nodiscard]] 84 | Value&& operator[](const std::string &key) && { 85 | return std::move(std::get(content)[key]); 86 | } 87 | 88 | [[nodiscard]] 89 | Value& operator[](const char *key) & { 90 | return std::get(content)[key]; 91 | } 92 | 93 | [[nodiscard]] 94 | Value&& operator[](const char *key) && { 95 | return std::move(std::get(content)[key]); 96 | } 97 | 98 | auto& operator=(u32 value) { 99 | this->content = value; 100 | return *this; 101 | } 102 | 103 | auto& operator=(const std::string &value) { 104 | this->content = value; 105 | return *this; 106 | } 107 | 108 | auto& operator=(const Set &value) { 109 | this->content = value; 110 | return *this; 111 | } 112 | 113 | [[nodiscard]] 114 | explicit operator const std::string&() const { 115 | return this->string(); 116 | } 117 | 118 | [[nodiscard]] 119 | explicit operator std::string() { 120 | return this->string(); 121 | } 122 | 123 | [[nodiscard]] 124 | explicit operator const u32&() const { 125 | return this->integer(); 126 | } 127 | 128 | [[nodiscard]] 129 | explicit operator u32() { 130 | return this->integer(); 131 | } 132 | 133 | bool operator==(const Value &other) const { 134 | return this->content == other.content; 135 | } 136 | 137 | bool operator!=(const Value &other) const { 138 | return this->content != other.content; 139 | } 140 | 141 | }; 142 | 143 | struct KeyValuePair { 144 | std::string key; 145 | Value value; 146 | }; 147 | 148 | [[nodiscard]] 149 | Value& operator[](const char *key) { 150 | return this->m_content[key]; 151 | } 152 | 153 | [[nodiscard]] 154 | const Value& operator[](const char *key) const { 155 | return this->m_content.at(key); 156 | } 157 | 158 | [[nodiscard]] 159 | Value& operator[](const std::string &key) { 160 | return this->m_content[key]; 161 | } 162 | 163 | [[nodiscard]] 164 | const Value& operator[](const std::string &key) const { 165 | return this->m_content.at(key); 166 | } 167 | 168 | [[nodiscard]] 169 | Set& get() { 170 | return this->m_content; 171 | } 172 | 173 | [[nodiscard]] 174 | const Set& get() const { 175 | return this->m_content; 176 | } 177 | 178 | [[nodiscard]] 179 | std::vector dump() const; 180 | 181 | [[nodiscard]] 182 | std::string format() const; 183 | 184 | bool operator==(const VDF &other) const { 185 | return this->m_content == other.m_content; 186 | } 187 | 188 | bool operator!=(const VDF &other) const { 189 | return this->m_content != other.m_content; 190 | } 191 | 192 | private: 193 | Set parse(const std::vector &data); 194 | 195 | Set m_content; 196 | }; 197 | 198 | static inline bool operator==(u8 byte, VDF::Type type) { 199 | return static_cast(byte) == type; 200 | } 201 | 202 | } -------------------------------------------------------------------------------- /lib/include/steam/helpers/file.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace steam::fs { 12 | 13 | class File { 14 | public: 15 | enum class Mode 16 | { 17 | Read, 18 | Write, 19 | Create 20 | }; 21 | 22 | explicit File(const std::fs::path &path, Mode mode) noexcept; 23 | File() noexcept; 24 | File(const File &) = delete; 25 | File(File &&other) noexcept; 26 | 27 | ~File(); 28 | 29 | File &operator=(File &&other) noexcept; 30 | 31 | 32 | [[nodiscard]] bool isValid() const { 33 | return this->m_file != nullptr && fs::exists(this->m_path) && !fs::isDirectory(this->m_path); 34 | } 35 | 36 | void seek(u64 offset); 37 | void close(); 38 | 39 | size_t readBuffer(u8 *buffer, size_t size); 40 | std::vector readBytes(size_t numBytes = 0); 41 | std::string readString(size_t numBytes = 0); 42 | 43 | void write(const u8 *buffer, size_t size); 44 | void write(const std::vector &bytes); 45 | void write(const std::string &string); 46 | 47 | [[nodiscard]] size_t getSize() const; 48 | void setSize(u64 size); 49 | 50 | void flush(); 51 | bool remove(); 52 | 53 | auto getHandle() { return this->m_file; } 54 | const std::fs::path &getPath() { return this->m_path; } 55 | 56 | void disableBuffering(); 57 | 58 | private: 59 | FILE *m_file; 60 | std::fs::path m_path; 61 | }; 62 | 63 | } -------------------------------------------------------------------------------- /lib/include/steam/helpers/fs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace std { 8 | namespace fs = std::filesystem; 9 | } 10 | 11 | namespace steam::fs { 12 | 13 | static inline bool exists(const std::fs::path &path) { 14 | std::error_code error; 15 | return std::filesystem::exists(path, error) && !error; 16 | } 17 | 18 | static inline bool createDirectories(const std::fs::path &path) { 19 | std::error_code error; 20 | return std::filesystem::create_directories(path, error) && !error; 21 | } 22 | 23 | static inline bool isRegularFile(const std::fs::path &path) { 24 | std::error_code error; 25 | return std::filesystem::is_regular_file(path, error) && !error; 26 | } 27 | 28 | static inline bool copyFile(const std::fs::path &from, const std::fs::path &to, std::fs::copy_options = std::fs::copy_options::none) { 29 | std::error_code error; 30 | return std::filesystem::copy_file(from, to, error) && !error; 31 | } 32 | 33 | static inline bool isDirectory(const std::fs::path &path) { 34 | std::error_code error; 35 | return std::filesystem::is_directory(path, error) && !error; 36 | } 37 | 38 | static inline bool remove(const std::fs::path &path) { 39 | std::error_code error; 40 | return std::filesystem::remove(path, error) && !error; 41 | } 42 | 43 | static inline uintmax_t getFileSize(const std::fs::path &path) { 44 | std::error_code error; 45 | auto size = std::filesystem::file_size(path, error); 46 | 47 | if (error) return 0; 48 | else return size; 49 | } 50 | 51 | static inline std::fs::path getHomeDirectory() { 52 | return std::getenv("HOME"); 53 | } 54 | 55 | static inline std::fs::path getSteamDirectory() { 56 | return getHomeDirectory() / ".steam" / "steam"; 57 | } 58 | 59 | bool isPathWritable(const std::fs::path &path); 60 | 61 | } -------------------------------------------------------------------------------- /lib/include/steam/helpers/hash.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace steam { 8 | 9 | template 10 | [[nodiscard]] u32 crc32(const auto &data, u32 initialValue = 0x00) { 11 | // Lookup table generation 12 | constexpr static auto Table = [] { 13 | std::array table = {0}; 14 | 15 | for (u32 i = 0; i < 256; i++) { 16 | u32 c = i; 17 | for (size_t j = 0; j < 8; j++) { 18 | if (c & 1) 19 | c = Polynomial ^ (c >> 1); 20 | else 21 | c >>= 1; 22 | } 23 | table[i] = c; 24 | } 25 | 26 | return table; 27 | }(); 28 | 29 | // CRC32 calculation 30 | u32 crc = initialValue; 31 | for (u8 byte : data) { 32 | crc = Table[(crc ^ byte) & 0xFF] ^ (crc >> 8); 33 | } 34 | 35 | return ~crc; 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /lib/include/steam/helpers/net.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | using CURL = void; 18 | struct curl_slist; 19 | 20 | namespace steam { 21 | 22 | template 23 | struct Response { 24 | i32 code; 25 | T body; 26 | }; 27 | 28 | template<> 29 | struct Response { 30 | i32 code; 31 | }; 32 | 33 | class Net { 34 | public: 35 | Net(); 36 | ~Net(); 37 | 38 | static constexpr u32 DefaultTimeout = 2'000; 39 | 40 | static void init(); 41 | static void exit(); 42 | 43 | std::future> getString(const std::string &url, u32 timeout = DefaultTimeout, const std::map &extraHeaders = {}, const std::string &body = {}); 44 | std::future> getJson(const std::string &url, u32 timeout = DefaultTimeout, const std::map &extraHeaders = {}, const std::string &body = {}); 45 | 46 | std::future> uploadFile(const std::string &url, const std::fs::path &filePath, u32 timeout = DefaultTimeout, const std::map &extraHeaders = {}, const std::string &body = {}); 47 | std::future> downloadFile(const std::string &url, const std::fs::path &filePath, u32 timeout = DefaultTimeout, const std::map &extraHeaders = {}, const std::string &body = {}); 48 | 49 | [[nodiscard]] std::string encode(const std::string &input); 50 | 51 | [[nodiscard]] float getProgress() const { return this->m_progress; } 52 | 53 | void cancel() { this->m_shouldCancel = true; } 54 | 55 | private: 56 | void setCommonSettings(std::string &response, const std::string &url, u32 timeout = 2000, const std::map &extraHeaders = {}, const std::string &body = {}); 57 | std::optional execute(); 58 | 59 | friend int progressCallback(void *contents, curl_off_t dlTotal, curl_off_t dlNow, curl_off_t ulTotal, curl_off_t ulNow); 60 | 61 | private: 62 | CURL *m_ctx; 63 | curl_slist *m_headers = nullptr; 64 | 65 | std::mutex m_transmissionActive; 66 | float m_progress = 0.0F; 67 | bool m_shouldCancel = false; 68 | }; 69 | 70 | } -------------------------------------------------------------------------------- /lib/include/steam/helpers/utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace steam { 6 | 7 | template struct overloaded : Ts... { using Ts::operator()...; }; 8 | template overloaded(Ts...) -> overloaded; 9 | 10 | bool isIntegerString(std::string string); 11 | 12 | } -------------------------------------------------------------------------------- /lib/source/api/steam_api.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace steam::api { 14 | 15 | static std::fs::path getShortcutsFilePath(const User &user) { 16 | return fs::getSteamDirectory() / "userdata" / std::to_string(user.getId()) / "config"; 17 | } 18 | 19 | static std::optional getShortcuts(const User &user) { 20 | auto configPath = getShortcutsFilePath(user); 21 | 22 | // Create backup of original shortcuts file if there haven't been any modifications done by us yet 23 | if (!fs::exists(configPath / "shortcuts.vdf.orig")) 24 | if (!fs::copyFile(configPath / "shortcuts.vdf", configPath / "shortcuts.vdf.orig")) { 25 | return std::nullopt; 26 | } 27 | 28 | // Create a backup of the current shortcuts file 29 | fs::remove(configPath / "shortcuts.vdf.bak"); 30 | if (!fs::copyFile(configPath / "shortcuts.vdf", configPath / "shortcuts.vdf.bak")) { 31 | return std::nullopt; 32 | } 33 | 34 | // Open shortcuts file 35 | auto shortcutsFile = fs::File(configPath / "shortcuts.vdf", fs::File::Mode::Write); 36 | if (!shortcutsFile.isValid()) { 37 | return std::nullopt; 38 | } 39 | 40 | // Parse shortcuts 41 | auto shortcuts = VDF(shortcutsFile.readBytes()); 42 | 43 | return shortcuts; 44 | } 45 | 46 | std::optional addGameShortcut(const User &user, const std::string &appName, const std::fs::path &exePath, const std::string &launchOptions, const std::vector &tags, bool hidden) { 47 | 48 | // Generate AppID 49 | auto appId = AppId(exePath, appName); 50 | 51 | // Parse shortcuts 52 | auto shortcuts = getShortcuts(user); 53 | if (!shortcuts) 54 | return std::nullopt; 55 | 56 | // Find the next free shortcut array index 57 | u32 nextShortcutId = 0; 58 | { 59 | for (const auto &[key, value] : (*shortcuts)["shortcuts"].set()) { 60 | if (!isIntegerString(key)) 61 | return std::nullopt; 62 | 63 | auto id = std::stoi(key); 64 | if (id > nextShortcutId) 65 | nextShortcutId = id; 66 | } 67 | 68 | nextShortcutId++; 69 | } 70 | 71 | // Add the new shortcut 72 | { 73 | VDF::Set tagsSet; 74 | { 75 | u32 index = 0; 76 | for (const auto &tag : tags) { 77 | tagsSet[std::to_string(index)] = tag; 78 | index++; 79 | } 80 | } 81 | 82 | VDF::Set shortcut; 83 | shortcut["AllowDesktopConfig"] = true; 84 | shortcut["AllowOverlay"] = true; 85 | shortcut["AppName"] = appName; 86 | shortcut["Devkit"] = false; 87 | shortcut["DevkitGameID"] = ""; 88 | shortcut["DevkitOverrideAppID"] = false; 89 | shortcut["Exe"] = fmt::format("\"{0}\"", exePath.string()); 90 | shortcut["FlatpakAppID"] = ""; 91 | shortcut["IsHidden"] = hidden; 92 | shortcut["LastPlayTime"] = 0; 93 | shortcut["LaunchOptions"] = launchOptions; 94 | shortcut["OpenVR"] = false; 95 | shortcut["ShortcutPath"] = ""; 96 | shortcut["StartDir"] = fmt::format("\"{0}\"", exePath.parent_path().string()); 97 | shortcut["appid"] = appId.getShortAppId(); 98 | shortcut["icon"] = ""; 99 | shortcut["tags"] = tagsSet; 100 | 101 | (*shortcuts)["shortcuts"][std::to_string(nextShortcutId)] = shortcut; 102 | } 103 | 104 | // Dump the shortcut data back to the shortcuts file 105 | auto shortcutsFile = fs::File(getShortcutsFilePath(user), fs::File::Mode::Create); 106 | shortcutsFile.write(shortcuts->dump()); 107 | 108 | return appId; 109 | } 110 | 111 | bool removeGameShortcut(const User &user, const AppId &appId) { 112 | auto shortcuts = getShortcuts(user); 113 | if (!shortcuts) 114 | return false; 115 | 116 | // Get number of shortcuts 117 | auto shortcutCount = (*shortcuts)["shortcuts"].set().size(); 118 | 119 | // Find shortcut with appid we want to remove 120 | std::string shortcutToRemoveKey; 121 | for (const auto &[key, value] : (*shortcuts)["shortcuts"].set()) { 122 | if (!value.isSet()) 123 | return false; 124 | 125 | const auto &game = value.set(); 126 | if (!game.contains("appid") || !game.at("appid").isInteger()) 127 | return false; 128 | 129 | if (game.at("appid").integer() == appId.getShortAppId()) { 130 | shortcutToRemoveKey = key; 131 | break; 132 | } 133 | } 134 | 135 | // Bail out if no shortcut with that appid has been found 136 | if (shortcutToRemoveKey.empty() || !isIntegerString(shortcutToRemoveKey)) 137 | return false; 138 | 139 | // Remove the shortcut 140 | shortcuts->get().erase(shortcutToRemoveKey); 141 | 142 | // Move all following entries backwards to keep the array contiguous 143 | auto shortcutIndex = std::stoi(shortcutToRemoveKey); 144 | for (size_t i = shortcutIndex; i < shortcutCount - 1; i++) { 145 | shortcuts->get()[std::to_string(i)] = shortcuts->get()[std::to_string(i + 1)]; 146 | shortcuts->get().erase(std::to_string(i + 1)); 147 | } 148 | 149 | return true; 150 | } 151 | 152 | bool enableProtonForApp(AppId appId, bool enabled) { 153 | auto configPath = fs::getSteamDirectory() / "config"; 154 | 155 | // Create backup of original config file if there haven't been any modifications done by us yet 156 | if (!fs::exists(configPath / "config.vdf.orig")) 157 | if (!fs::copyFile(configPath / "config.vdf", configPath / "config.vdf.orig")) return false; 158 | 159 | // Create a backup of the current config file 160 | if (!fs::remove(configPath / "config.vdf.bak")) return false; 161 | if (!fs::copyFile(configPath / "config.vdf", configPath / "config.vdf.bak")) return false; 162 | 163 | auto configFile = fs::File(configPath / "config.vdf", fs::File::Mode::Read); 164 | if (!configFile.isValid()) 165 | return false; 166 | 167 | auto config = KeyValues(configFile.readString()); 168 | 169 | // Create or remove config entry for proton 170 | if (enabled) { 171 | KeyValues::Set entry; 172 | 173 | entry["name"] = "proton_experimental"; 174 | entry["config"] = ""; 175 | entry["Priority"] = "250"; 176 | 177 | config["InstallConfigStore"]["Software"]["Valve"]["Steam"]["CompatToolMapping"][std::to_string(appId.getShortAppId())] = entry; 178 | } else { 179 | config["InstallConfigStore"]["Software"]["Valve"]["Steam"]["CompatToolMapping"].set().erase(std::to_string(appId.getShortAppId())); 180 | } 181 | 182 | // Dump data to config file 183 | configFile = fs::File(configPath / "config.vdf", fs::File::Mode::Create); 184 | configFile.write(config.dump()); 185 | 186 | return true; 187 | } 188 | 189 | bool restartSteam() { 190 | // Get status information of the current process 191 | auto statFile = fs::File("/proc/self/stat", fs::File::Mode::Read); 192 | 193 | do { 194 | if (!statFile.isValid()) 195 | return false; 196 | 197 | i32 pid; 198 | char comm[0xFF]; 199 | char state; 200 | i32 ppid; 201 | 202 | // Parse stat data 203 | fscanf(statFile.getHandle(), "%d %s %c %d", &pid, comm, &state, &ppid); 204 | 205 | // Bail out when we reached the init process 206 | if (ppid <= 1) 207 | return false; 208 | 209 | if (std::string(comm) == "(steam)") { 210 | // If the steam executable is reached, kill it 211 | kill(pid, SIGKILL); 212 | } 213 | else { 214 | // If the executable isn't steam yet, read the stat file of the parent process 215 | statFile = fs::File(fmt::format("/proc/{}/stat", ppid), fs::File::Mode::Read); 216 | } 217 | } while (true); 218 | } 219 | 220 | } -------------------------------------------------------------------------------- /lib/source/api/steam_grid_api.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace steam::api { 6 | 7 | std::future> SteamGridDBAPI::search(const std::string &keyword) { 8 | return std::async(std::launch::async, [=, this]() -> std::vector { 9 | auto response = this->m_net.getJson( 10 | SteamGridDBAPI::BaseUrl + "/search/autocomplete/" + this->m_net.encode(keyword), 11 | Net::DefaultTimeout, 12 | { { "Authorization", "Bearer " + this->m_apiKey } } 13 | ).get(); 14 | 15 | if (response.code != 200) 16 | return { }; 17 | 18 | auto body = response.body; 19 | if (!body.contains("success") || !body.contains("data") || body["success"] == false) 20 | return { }; 21 | 22 | std::vector result; 23 | 24 | for (const auto &entry : body["data"]) { 25 | if (!(entry.contains("types") && entry.contains("id") && entry.contains("name") && entry.contains("verified"))) 26 | continue; 27 | 28 | if (!entry["types"].is_array()) 29 | continue; 30 | if (!entry["id"].is_number()) 31 | continue; 32 | if (!entry["name"].is_string()) 33 | continue; 34 | if (!entry["verified"].is_boolean()) 35 | continue; 36 | 37 | result.push_back({ entry["types"], AppId(entry["id"]), entry["name"], entry["verified"] }); 38 | } 39 | 40 | return result; 41 | }); 42 | } 43 | 44 | static std::vector parseImageResults(const nlohmann::json &data) { 45 | std::vector result; 46 | 47 | for (const auto &entry : data) { 48 | if (!entry.contains("id") || !entry["id"].is_number()) 49 | continue; 50 | if (!entry.contains("score") || !entry["score"].is_number()) 51 | continue; 52 | if (!entry.contains("url") || !entry["url"].is_string()) 53 | continue; 54 | if (!entry.contains("thumb") || !entry["thumb"].is_string()) 55 | continue; 56 | 57 | result.push_back({ 58 | AppId(entry["id"]), 59 | entry["score"], 60 | entry["style"], 61 | entry["url"], 62 | entry["thumb"] 63 | }); 64 | } 65 | 66 | return result; 67 | } 68 | 69 | std::future> SteamGridDBAPI::getGrids(const AppId &appId) { 70 | return std::async(std::launch::async, [=, this]() -> std::vector { 71 | auto response = this->m_net.getJson( 72 | SteamGridDBAPI::BaseUrl + "/grids/game/" + std::to_string(appId.getAppId()), 73 | Net::DefaultTimeout, 74 | { { "Authorization", "Bearer " + this->m_apiKey } } 75 | ).get(); 76 | 77 | if (response.code != 200) 78 | return { }; 79 | 80 | auto body = response.body; 81 | if (!body.contains("success") || !body.contains("data") || body["success"] == false) 82 | return { }; 83 | 84 | return parseImageResults(body["data"]); 85 | }); 86 | } 87 | 88 | std::future> SteamGridDBAPI::getHeroes(const AppId &appId) { 89 | return std::async(std::launch::async, [=, this]() -> std::vector { 90 | auto response = this->m_net.getJson( 91 | SteamGridDBAPI::BaseUrl + "/heroes/game/" + std::to_string(appId.getAppId()), 92 | Net::DefaultTimeout, 93 | { { "Authorization", "Bearer " + this->m_apiKey } } 94 | ).get(); 95 | 96 | if (response.code != 200) 97 | return { }; 98 | 99 | auto body = response.body; 100 | if (!body.contains("success") || !body.contains("data") || body["success"] == false) 101 | return { }; 102 | 103 | return parseImageResults(body["data"]); 104 | }); 105 | } 106 | 107 | std::future> SteamGridDBAPI::getLogos(const AppId &appId) { 108 | return std::async(std::launch::async, [=, this]() -> std::vector { 109 | auto response = this->m_net.getJson( 110 | SteamGridDBAPI::BaseUrl + "/logos/game/" + std::to_string(appId.getAppId()), 111 | Net::DefaultTimeout, 112 | { { "Authorization", "Bearer " + this->m_apiKey } } 113 | ).get(); 114 | 115 | if (response.code != 200) 116 | return { }; 117 | 118 | auto body = response.body; 119 | if (!body.contains("success") || !body.contains("data") || body["success"] == false) 120 | return { }; 121 | 122 | return parseImageResults(body["data"]); 123 | }); 124 | } 125 | 126 | std::future> SteamGridDBAPI::getIcons(const AppId &appId) { 127 | return std::async(std::launch::async, [=, this]() -> std::vector { 128 | auto response = this->m_net.getJson( 129 | SteamGridDBAPI::BaseUrl + "/icons/game/" + std::to_string(appId.getAppId()), 130 | Net::DefaultTimeout, 131 | { { "Authorization", "Bearer " + this->m_apiKey } } 132 | ).get(); 133 | 134 | if (response.code != 200) 135 | return { }; 136 | 137 | auto body = response.body; 138 | if (!body.contains("success") || !body.contains("data") || body["success"] == false) 139 | return { }; 140 | 141 | return parseImageResults(body["data"]); 142 | }); 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /lib/source/file_formats/keyvalues.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace steam { 10 | 11 | std::pair parseElement(std::u32string_view data); 12 | std::string dumpElement(const std::string &key, const KeyValues::Value &value, u32 indent); 13 | 14 | std::pair parseString(std::u32string_view data) { 15 | std::u32string result; 16 | size_t advance = 0; 17 | 18 | if (!data.starts_with('"')) 19 | return { { }, 0 }; 20 | 21 | for (u32 i = 1; i < data.length(); i++) { 22 | advance++; 23 | auto character = data[i]; 24 | 25 | if (character < 0x7F) { 26 | // If this is an ASCII character... 27 | 28 | if (character == '\\') { 29 | if (i == (data.length() - 1)) 30 | return { { }, 0 }; 31 | 32 | switch (data[i + 1]) { 33 | case 'n': result += '\n'; break; 34 | case 't': result += '\t'; break; 35 | case '\\': result += '\\'; break; 36 | case '"': result += '"'; break; 37 | default: return { { }, 0 }; 38 | } 39 | i++; 40 | advance++; 41 | } else if (character == '"') { 42 | break; 43 | } else { 44 | result += character; 45 | } 46 | } else { 47 | result += character; 48 | } 49 | } 50 | 51 | return { result, advance + 1 }; 52 | } 53 | 54 | size_t consumeWhitespace(std::u32string_view data) { 55 | size_t advance = 0; 56 | 57 | for (const auto &character : data) { 58 | if (character < 0x7F && std::isspace(static_cast(character & 0x7F))) { 59 | advance++; 60 | } else { 61 | break; 62 | } 63 | } 64 | 65 | return advance; 66 | } 67 | 68 | std::pair parseSet(std::u32string_view data) { 69 | KeyValues::Set result; 70 | size_t advance = 0; 71 | 72 | if (!data.starts_with('{')) 73 | return { { }, 0 }; 74 | 75 | advance++; 76 | advance += consumeWhitespace(data.substr(advance)); 77 | 78 | while (advance < data.length() && data[advance] != '}') { 79 | auto [keyValue, valueBytesUsed] = parseElement(data.substr(advance)); 80 | if (valueBytesUsed == 0) 81 | return { { }, 0 }; 82 | 83 | advance += valueBytesUsed; 84 | advance += consumeWhitespace(data.substr(advance)); 85 | 86 | result.emplace(keyValue.key, keyValue.value); 87 | } 88 | 89 | return { result, advance + 1 }; 90 | } 91 | 92 | std::pair parseElement(std::u32string_view data) { 93 | KeyValues::KeyValuePair result; 94 | size_t advance = 0; 95 | 96 | std::wstring_convert, char32_t> converter; 97 | 98 | advance += consumeWhitespace(data); 99 | 100 | { 101 | const auto [value, bytesUsed] = parseString(data.substr(advance)); 102 | if (bytesUsed == 0) 103 | return { { }, 0 }; 104 | 105 | result.key = converter.to_bytes(value); 106 | advance += bytesUsed; 107 | } 108 | 109 | advance += consumeWhitespace(data.substr(advance)); 110 | 111 | switch (data[advance]) { 112 | case '"': { 113 | auto [value, bytesUsed] = parseString(data.substr(advance)); 114 | result.value.content = converter.to_bytes(value); 115 | advance += bytesUsed; 116 | break; 117 | } 118 | case '{': { 119 | auto [value, bytesUsed] = parseSet(data.substr(advance)); 120 | result.value.content = value; 121 | advance += bytesUsed; 122 | break; 123 | } 124 | default: 125 | return { { }, 0 }; 126 | } 127 | 128 | return { result, advance }; 129 | } 130 | 131 | KeyValues::Set KeyValues::parse(const std::string &data) { 132 | std::wstring_convert, char32_t> converter; 133 | const auto convertedData = converter.from_bytes(data); 134 | 135 | KeyValues::Set result; 136 | size_t advance = 0; 137 | 138 | while (advance < convertedData.size()) { 139 | auto [keyValue, bytesUsed] = parseElement(convertedData.substr(advance)); 140 | if (bytesUsed == 0) 141 | return { }; 142 | 143 | advance += bytesUsed; 144 | 145 | result.emplace(keyValue.key, keyValue.value); 146 | 147 | advance += consumeWhitespace(convertedData.substr(advance)); 148 | } 149 | 150 | return result; 151 | } 152 | 153 | std::string dumpString(const std::string &string) { 154 | std::u32string result; 155 | 156 | std::wstring_convert, char32_t> converter; 157 | const auto convertedString = converter.from_bytes(string); 158 | 159 | for (const auto &character : convertedString) { 160 | switch (character) { 161 | case '\\': result += U"\\\\"; break; 162 | case '\"': result += U"\\\""; break; 163 | case '\t': result += U"\\t"; break; 164 | case '\n': result += U"\\n"; break; 165 | default: result += character; break; 166 | } 167 | } 168 | 169 | return fmt::format("\"{0}\"", converter.to_bytes(result)); 170 | } 171 | 172 | std::string dumpSet(const KeyValues::Set &set, u32 indent) { 173 | std::string result; 174 | 175 | result += fmt::format("{0: >{1}}{{\n", "", indent); 176 | 177 | for (const auto &[key, value] : set) { 178 | result += dumpElement(key, value, indent + 4); 179 | } 180 | 181 | result += fmt::format("{0: >{1}}}}", "", indent); 182 | 183 | return result; 184 | } 185 | 186 | std::string dumpElement(const std::string &key, const KeyValues::Value &value, u32 indent) { 187 | std::string result; 188 | 189 | result += fmt::format("{0: >{1}}{2}", "", indent, dumpString(key)); 190 | 191 | result += std::visit(overloaded { 192 | [](const std::string &value) { 193 | return fmt::format("\t\t{0}\n", dumpString(value)); 194 | }, 195 | [&indent](const KeyValues::Set &value) { 196 | return fmt::format("\n{0}\n", dumpSet(value, indent)); 197 | } 198 | }, value.content); 199 | 200 | return result; 201 | } 202 | 203 | std::string KeyValues::dump() const { 204 | std::string result; 205 | 206 | for (const auto &[key, value] : this->m_content) { 207 | result += dumpElement(key, value, 0); 208 | } 209 | 210 | return result; 211 | } 212 | } -------------------------------------------------------------------------------- /lib/source/file_formats/vdf.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace steam { 10 | 11 | std::pair parseElement(std::span data); 12 | 13 | std::pair parseString(std::span data) { 14 | std::string result; 15 | size_t advance = 0; 16 | 17 | for (u8 byte : data) { 18 | if (byte == 0x00) break; 19 | result += char(byte); 20 | advance++; 21 | } 22 | 23 | if (data[advance] != 0x00) return { {}, 0 }; 24 | 25 | advance++; 26 | 27 | return { result, advance }; 28 | } 29 | 30 | std::pair parseInteger(std::span data) { 31 | u32 result; 32 | size_t advance = 0; 33 | 34 | if (data.size() < 4) return { {}, 0 }; 35 | 36 | return { (data[0]) | (data[1] << 8) | (data[2] << 16) | (data[3] << 24), 4 }; 37 | } 38 | 39 | std::pair parseKeyStringValue(std::span data) { 40 | VDF::KeyValuePair result; 41 | size_t advance = 0; 42 | 43 | { 44 | auto [key, bytesUsed] = parseString(data); 45 | result.key = key; 46 | advance += bytesUsed; 47 | } 48 | 49 | { 50 | auto [value, bytesUsed] = parseString({ &data[advance], data.size() - advance }); 51 | result.value.content = value; 52 | advance += bytesUsed; 53 | } 54 | 55 | return { result, advance }; 56 | } 57 | 58 | std::pair parseKeyIntegerValue(std::span data) { 59 | VDF::KeyValuePair result; 60 | size_t advance = 0; 61 | 62 | { 63 | auto [key, bytesUsed] = parseString(data); 64 | result.key = key; 65 | advance += bytesUsed; 66 | } 67 | 68 | { 69 | auto [value, bytesUsed] = parseInteger({ &data[advance], data.size() - advance }); 70 | result.value.content = value; 71 | advance += bytesUsed; 72 | } 73 | 74 | return { result, advance }; 75 | } 76 | 77 | std::pair parseSet(std::span data) { 78 | VDF::KeyValuePair result; 79 | size_t advance = 0; 80 | 81 | { 82 | auto [value, usedBytes] = parseString(data); 83 | 84 | result.key = value; 85 | advance += usedBytes; 86 | } 87 | 88 | result.value.content = VDF::Set{}; 89 | 90 | while (true) { 91 | if (advance >= data.size()) 92 | return { {}, 0 }; 93 | 94 | if (data[advance] == VDF::Type::EndSet) { 95 | advance++; 96 | break; 97 | } 98 | 99 | auto [element, usedBytes] = parseElement({ &data[advance], data.size() - advance }); 100 | if (usedBytes == 0) 101 | return { { }, 0 }; 102 | 103 | advance += usedBytes; 104 | std::get(result.value.content).emplace(element.key, element.value); 105 | } 106 | 107 | return { result, advance }; 108 | } 109 | 110 | std::pair parseElement(std::span data) { 111 | VDF::KeyValuePair result; 112 | size_t advance = 0; 113 | 114 | const auto type = static_cast(data[0]); 115 | const auto span = std::span{ &data[1], data.size() - 1 }; 116 | 117 | switch (type) { 118 | case VDF::Type::Set: 119 | std::tie(result, advance) = parseSet(span); 120 | break; 121 | case VDF::Type::Integer: 122 | std::tie(result, advance) = parseKeyIntegerValue(span); 123 | break; 124 | case VDF::Type::String: 125 | std::tie(result, advance) = parseKeyStringValue(span); 126 | break; 127 | case VDF::Type::EndSet: 128 | break; 129 | default: 130 | return { {}, 0 }; 131 | } 132 | 133 | return { result, advance + 1 }; 134 | } 135 | 136 | VDF::Set VDF::parse(const std::vector &data) { 137 | Set result; 138 | 139 | u64 offset = 0; 140 | while (offset < data.size()) { 141 | auto [element, bytesUsed] = parseElement({ &data[offset], data.size() - offset }); 142 | 143 | if (bytesUsed == 0) 144 | return { }; 145 | 146 | if (element.key.empty()) 147 | break; 148 | 149 | offset += bytesUsed; 150 | result.emplace(element.key, element.value); 151 | } 152 | 153 | return result; 154 | } 155 | 156 | void dumpKey(VDF::Type type, const std::string &key, std::vector &result) { 157 | if (key.empty()) return; 158 | 159 | result.push_back(static_cast(type)); 160 | 161 | std::copy(key.begin(), key.end(), std::back_inserter(result)); 162 | result.push_back(0x00); 163 | } 164 | 165 | void dumpString(const std::string &key, const std::string &content, std::vector &result) { 166 | dumpKey(VDF::Type::String, key, result); 167 | 168 | std::copy(content.begin(), content.end(), std::back_inserter(result)); 169 | result.push_back(0x00); 170 | } 171 | 172 | void dumpInteger(const std::string &key, u32 content, std::vector &result) { 173 | dumpKey(VDF::Type::Integer, key, result); 174 | 175 | result.push_back((content >> 0) & 0xFF); 176 | result.push_back((content >> 8) & 0xFF); 177 | result.push_back((content >> 16) & 0xFF); 178 | result.push_back((content >> 24) & 0xFF); 179 | } 180 | 181 | void dumpElement(const std::string &key, const VDF::Value &value, std::vector &result); 182 | 183 | void dumpSet(const std::string &setKey, const VDF::Set &content, std::vector &result) { 184 | dumpKey(VDF::Type::Set, setKey, result); 185 | 186 | for (const auto &[key, value] : content) { 187 | dumpElement(key, value, result); 188 | } 189 | 190 | result.push_back(static_cast(VDF::Type::EndSet)); 191 | } 192 | 193 | void dumpElement(const std::string &key, const VDF::Value &value, std::vector &result) { 194 | std::visit(overloaded { 195 | [&](const std::string &string) { 196 | dumpString(key, string, result); 197 | }, 198 | [&](const u32 &integer) { 199 | dumpInteger(key, integer, result); 200 | }, 201 | [&](const VDF::Set &set) { 202 | dumpSet(key, set, result); 203 | } 204 | }, value.content); 205 | } 206 | 207 | std::vector VDF::dump() const { 208 | std::vector result; 209 | 210 | dumpSet("", this->m_content, result); 211 | 212 | return result; 213 | } 214 | 215 | std::string formatImpl(const VDF::Set &content, u32 indent) { 216 | std::string result; 217 | 218 | for (auto &[key, value] : content) { 219 | result += fmt::format(",\n{0: >{1}}\"{2}\": ", "", indent, key); 220 | std::visit(steam::overloaded { 221 | [&](const std::string& x) { 222 | result += fmt::format("\"{0}\"", x); 223 | }, 224 | [&](const steam::u32& x) { 225 | result += fmt::format("{0}", x); 226 | }, 227 | [&](const VDF::Set& x) { 228 | result += "{"; 229 | { 230 | auto content = formatImpl(x, indent + 4); 231 | if (!content.empty()) 232 | result += content.substr(1); 233 | } 234 | result += fmt::format("\n{0: >{1}}}}", "", indent); 235 | } 236 | }, value.content); 237 | } 238 | 239 | return result; 240 | } 241 | 242 | std::string VDF::format() const { 243 | return fmt::format("{{{0}\n}}", formatImpl(this->m_content, 4).substr(1)); 244 | } 245 | 246 | } -------------------------------------------------------------------------------- /lib/source/helpers/file.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace steam::fs { 5 | 6 | File::File(const std::fs::path &path, Mode mode) noexcept : m_path(path) { 7 | if (mode == File::Mode::Read) 8 | this->m_file = fopen64(path.string().c_str(), "rb"); 9 | else if (mode == File::Mode::Write) 10 | this->m_file = fopen64(path.string().c_str(), "r+b"); 11 | 12 | if (mode == File::Mode::Create || (mode == File::Mode::Write && this->m_file == nullptr)) 13 | this->m_file = fopen64(path.string().c_str(), "w+b"); 14 | } 15 | 16 | File::File() noexcept { 17 | this->m_file = nullptr; 18 | } 19 | 20 | File::File(File &&other) noexcept { 21 | this->m_file = other.m_file; 22 | other.m_file = nullptr; 23 | } 24 | 25 | File::~File() { 26 | this->close(); 27 | } 28 | 29 | File &File::operator=(File &&other) noexcept { 30 | this->m_file = other.m_file; 31 | other.m_file = nullptr; 32 | 33 | this->m_path = std::move(other.m_path); 34 | 35 | return *this; 36 | } 37 | 38 | 39 | void File::seek(u64 offset) { 40 | fseeko64(this->m_file, offset, SEEK_SET); 41 | } 42 | 43 | void File::close() { 44 | if (isValid()) { 45 | std::fclose(this->m_file); 46 | this->m_file = nullptr; 47 | } 48 | } 49 | 50 | size_t File::readBuffer(u8 *buffer, size_t size) { 51 | if (!isValid()) return 0; 52 | 53 | return fread(buffer, size, 1, this->m_file); 54 | } 55 | 56 | std::vector File::readBytes(size_t numBytes) { 57 | if (!isValid()) return {}; 58 | 59 | auto size = numBytes ?: getSize(); 60 | if (size == 0) return {}; 61 | 62 | std::vector bytes(size); 63 | auto bytesRead = fread(bytes.data(), 1, bytes.size(), this->m_file); 64 | 65 | bytes.resize(bytesRead); 66 | 67 | return bytes; 68 | } 69 | 70 | std::string File::readString(size_t numBytes) { 71 | if (!isValid()) return {}; 72 | 73 | if (getSize() == 0) return {}; 74 | 75 | auto bytes = readBytes(numBytes); 76 | 77 | if (bytes.empty()) 78 | return ""; 79 | 80 | return { reinterpret_cast(bytes.data()), bytes.size() }; 81 | } 82 | 83 | void File::write(const u8 *buffer, size_t size) { 84 | if (!isValid()) return; 85 | 86 | std::fwrite(buffer, size, 1, this->m_file); 87 | } 88 | 89 | void File::write(const std::vector &bytes) { 90 | if (!isValid()) return; 91 | 92 | std::fwrite(bytes.data(), 1, bytes.size(), this->m_file); 93 | } 94 | 95 | void File::write(const std::string &string) { 96 | if (!isValid()) return; 97 | 98 | std::fwrite(string.data(), string.size(), 1, this->m_file); 99 | } 100 | 101 | size_t File::getSize() const { 102 | if (!isValid()) return 0; 103 | 104 | auto startPos = ftello64(this->m_file); 105 | fseeko64(this->m_file, 0, SEEK_END); 106 | auto size = ftello64(this->m_file); 107 | fseeko64(this->m_file, startPos, SEEK_SET); 108 | 109 | if (size < 0) 110 | return 0; 111 | 112 | return size; 113 | } 114 | 115 | void File::setSize(u64 size) { 116 | if (!isValid()) return; 117 | 118 | ftruncate64(fileno(this->m_file), size); 119 | } 120 | 121 | void File::flush() { 122 | std::fflush(this->m_file); 123 | } 124 | 125 | bool File::remove() { 126 | this->close(); 127 | return std::remove(this->m_path.string().c_str()) == 0; 128 | } 129 | 130 | void File::disableBuffering() { 131 | if (!isValid()) return; 132 | 133 | std::setvbuf(this->m_file, nullptr, _IONBF, 0); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /lib/source/helpers/net.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | namespace steam { 17 | 18 | void Net::init() { 19 | curl_global_init(CURL_GLOBAL_ALL); 20 | } 21 | 22 | void Net::exit() { 23 | curl_global_cleanup(); 24 | } 25 | 26 | Net::Net() { 27 | this->m_ctx = curl_easy_init(); 28 | } 29 | 30 | Net::~Net() { 31 | curl_easy_cleanup(this->m_ctx); 32 | } 33 | 34 | static size_t writeToString(void *contents, size_t size, size_t nmemb, void *userdata) { 35 | static_cast(userdata)->append((char *)contents, size * nmemb); 36 | return size * nmemb; 37 | } 38 | 39 | static size_t readFromFile(void *contents, size_t size, size_t nmemb, void *userdata) { 40 | FILE *file = static_cast(userdata); 41 | 42 | return fread(contents, size, nmemb, file); 43 | } 44 | 45 | static size_t writeToFile(void *contents, size_t size, size_t nmemb, void *userdata) { 46 | FILE *file = static_cast(userdata); 47 | 48 | return fwrite(contents, size, nmemb, file); 49 | } 50 | 51 | int progressCallback(void *contents, curl_off_t dlTotal, curl_off_t dlNow, curl_off_t ulTotal, curl_off_t ulNow) { 52 | auto &net = *static_cast(contents); 53 | 54 | if (dlTotal > 0) 55 | net.m_progress = float(dlNow) / dlTotal; 56 | else if (ulTotal > 0) 57 | net.m_progress = float(ulNow) / ulTotal; 58 | else 59 | net.m_progress = 0.0F; 60 | 61 | return net.m_shouldCancel ? CURLE_ABORTED_BY_CALLBACK : CURLE_OK; 62 | } 63 | 64 | void Net::setCommonSettings(std::string &response, const std::string &url, u32 timeout, const std::map &extraHeaders, const std::string &body) { 65 | this->m_headers = curl_slist_append(this->m_headers, "Cache-Control: no-cache"); 66 | 67 | if (!extraHeaders.empty()) 68 | for (const auto &[key, value] : extraHeaders) { 69 | std::string entry = key; 70 | entry += ": "; 71 | entry += value; 72 | 73 | this->m_headers = curl_slist_append(this->m_headers, entry.c_str()); 74 | } 75 | 76 | if (!body.empty()) 77 | curl_easy_setopt(this->m_ctx, CURLOPT_POSTFIELDS, body.c_str()); 78 | 79 | curl_easy_setopt(this->m_ctx, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); 80 | curl_easy_setopt(this->m_ctx, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); 81 | curl_easy_setopt(this->m_ctx, CURLOPT_URL, url.c_str()); 82 | curl_easy_setopt(this->m_ctx, CURLOPT_FOLLOWLOCATION, 1L); 83 | curl_easy_setopt(this->m_ctx, CURLOPT_HTTPHEADER, this->m_headers); 84 | curl_easy_setopt(this->m_ctx, CURLOPT_USERAGENT, "libsteam/1.0"); 85 | curl_easy_setopt(this->m_ctx, CURLOPT_DEFAULT_PROTOCOL, "https"); 86 | curl_easy_setopt(this->m_ctx, CURLOPT_WRITEFUNCTION, writeToString); 87 | curl_easy_setopt(this->m_ctx, CURLOPT_SSL_VERIFYPEER, 1L); 88 | curl_easy_setopt(this->m_ctx, CURLOPT_SSL_VERIFYHOST, 2L); 89 | curl_easy_setopt(this->m_ctx, CURLOPT_WRITEDATA, &response); 90 | curl_easy_setopt(this->m_ctx, CURLOPT_TIMEOUT_MS, 0L); 91 | curl_easy_setopt(this->m_ctx, CURLOPT_CONNECTTIMEOUT_MS, timeout); 92 | curl_easy_setopt(this->m_ctx, CURLOPT_XFERINFODATA, this); 93 | curl_easy_setopt(this->m_ctx, CURLOPT_XFERINFOFUNCTION, progressCallback); 94 | curl_easy_setopt(this->m_ctx, CURLOPT_NOSIGNAL, 1L); 95 | curl_easy_setopt(this->m_ctx, CURLOPT_NOPROGRESS, 0L); 96 | } 97 | 98 | std::optional Net::execute() { 99 | CURLcode result = curl_easy_perform(this->m_ctx); 100 | if (result != CURLE_OK) 101 | fmt::print("Net request failed with error {0}: '{1}'", result, curl_easy_strerror(result)); 102 | 103 | i32 responseCode = 0; 104 | curl_easy_getinfo(this->m_ctx, CURLINFO_RESPONSE_CODE, &responseCode); 105 | 106 | curl_slist_free_all(this->m_headers); 107 | this->m_headers = nullptr; 108 | this->m_progress = 0.0F; 109 | this->m_shouldCancel = false; 110 | 111 | if (result != CURLE_OK) 112 | return std::nullopt; 113 | else 114 | return responseCode; 115 | } 116 | 117 | std::future> Net::getString(const std::string &url, u32 timeout, const std::map &extraHeaders, const std::string &body) { 118 | this->m_transmissionActive.lock(); 119 | 120 | return std::async(std::launch::async, [=, this] { 121 | std::string response; 122 | 123 | curl_easy_setopt(this->m_ctx, CURLOPT_CUSTOMREQUEST, "GET"); 124 | setCommonSettings(response, url, timeout, extraHeaders, body); 125 | 126 | auto responseCode = execute(); 127 | 128 | this->m_transmissionActive.unlock(); 129 | 130 | return Response { responseCode.value_or(0), response }; 131 | }); 132 | } 133 | 134 | std::future> Net::getJson(const std::string &url, u32 timeout, const std::map &extraHeaders, const std::string &body) { 135 | this->m_transmissionActive.lock(); 136 | 137 | return std::async(std::launch::async, [=, this] { 138 | std::string response; 139 | 140 | curl_easy_setopt(this->m_ctx, CURLOPT_CUSTOMREQUEST, "GET"); 141 | setCommonSettings(response, url, timeout, extraHeaders, body); 142 | 143 | auto responseCode = execute(); 144 | 145 | this->m_transmissionActive.unlock(); 146 | 147 | return Response { responseCode.value_or(0), nlohmann::json::parse(response, nullptr, false) }; 148 | }); 149 | } 150 | 151 | std::future> Net::uploadFile(const std::string &url, const std::fs::path &filePath, u32 timeout, const std::map &extraHeaders, const std::string &body) { 152 | this->m_transmissionActive.lock(); 153 | 154 | return std::async(std::launch::async, [=, this] { 155 | std::string response; 156 | 157 | fs::File file(filePath.string(), fs::File::Mode::Read); 158 | if (!file.isValid()) 159 | return Response { 400, {} }; 160 | 161 | curl_mime *mime = curl_mime_init(this->m_ctx); 162 | curl_mimepart *part = curl_mime_addpart(mime); 163 | 164 | auto fileName = filePath.filename().string(); 165 | curl_mime_data_cb( 166 | part, file.getSize(), [](char *buffer, size_t size, size_t nitems, void *arg) -> size_t { 167 | auto file = static_cast(arg); 168 | return fread(buffer, size, nitems, file); }, [](void *arg, curl_off_t offset, int origin) -> int { 169 | auto file = static_cast(arg); 170 | fseek(file, offset, origin); 171 | return CURL_SEEKFUNC_OK; }, [](void *arg) { 172 | auto file = static_cast(arg); 173 | fclose(file); }, file.getHandle()); 174 | curl_mime_filename(part, fileName.c_str()); 175 | curl_mime_name(part, "file"); 176 | 177 | setCommonSettings(response, url, timeout, extraHeaders, body); 178 | curl_easy_setopt(this->m_ctx, CURLOPT_MIMEPOST, mime); 179 | curl_easy_setopt(this->m_ctx, CURLOPT_CUSTOMREQUEST, "POST"); 180 | 181 | auto responseCode = execute(); 182 | 183 | this->m_transmissionActive.unlock(); 184 | 185 | return Response { responseCode.value_or(0), response }; 186 | }); 187 | } 188 | 189 | std::future> Net::downloadFile(const std::string &url, const std::fs::path &filePath, u32 timeout, const std::map &extraHeaders, const std::string &body) { 190 | this->m_transmissionActive.lock(); 191 | 192 | return std::async(std::launch::async, [=, this] { 193 | std::string response; 194 | 195 | fs::File file(filePath.string(), fs::File::Mode::Create); 196 | if (!file.isValid()) 197 | return Response { 400 }; 198 | 199 | setCommonSettings(response, url, timeout, extraHeaders, body); 200 | curl_easy_setopt(this->m_ctx, CURLOPT_CUSTOMREQUEST, "GET"); 201 | curl_easy_setopt(this->m_ctx, CURLOPT_WRITEFUNCTION, writeToFile); 202 | curl_easy_setopt(this->m_ctx, CURLOPT_WRITEDATA, file.getHandle()); 203 | auto responseCode = execute(); 204 | 205 | this->m_transmissionActive.unlock(); 206 | 207 | return Response { responseCode.value_or(0) }; 208 | }); 209 | } 210 | 211 | std::string Net::encode(const std::string &input) { 212 | auto escapedString = curl_easy_escape(this->m_ctx, input.c_str(), std::strlen(input.c_str())); 213 | 214 | if (escapedString != nullptr) { 215 | std::string output = escapedString; 216 | curl_free(escapedString); 217 | 218 | return output; 219 | } 220 | 221 | return {}; 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /lib/source/helpers/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace steam { 6 | 7 | bool isIntegerString(std::string string) { 8 | if (string.starts_with('-')) 9 | string = string.substr(1); 10 | 11 | return std::all_of(string.begin(), string.end(), ::isdigit); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(test_app) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | 6 | set(CMAKE_SKIP_BUILD_RPATH FALSE) 7 | set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) 8 | set(CMAKE_INSTALL_RPATH ".") 9 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) 10 | 11 | add_executable(test_app 12 | source/main.cpp 13 | ) 14 | 15 | target_link_libraries(test_app PUBLIC libsteam) 16 | 17 | set_target_properties(test_app 18 | PROPERTIES 19 | ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" 20 | LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" 21 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" 22 | ) 23 | 24 | install(TARGETS test_app) -------------------------------------------------------------------------------- /test/source/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | int main() { 9 | 10 | // Print all users 11 | auto users = steam::api::User::getUsers(); 12 | fmt::print("Users:\n"); 13 | for (const auto &user : users) { 14 | fmt::print("{} -> {}\n", user.getId(), user.getName().c_str()); 15 | } 16 | 17 | // Print all app ids 18 | auto appIds = steam::api::AppId::getAppIds(users.front()); 19 | fmt::print("AppIds:\n"); 20 | for (const auto &appId : appIds) { 21 | fmt::print("{}\n", appId.getAppId()); 22 | } 23 | 24 | // Add new Shortcut 25 | auto appId = steam::api::addGameShortcut(users.front(), "Hello World", "/home/deck/", "Test Options"); 26 | if (!appId.has_value()) { 27 | printf("Failed to add game shortcut!\n"); 28 | return EXIT_FAILURE; 29 | } 30 | 31 | // Enable proton for new shortcut 32 | if (!steam::api::enableProtonForApp(*appId, true)) { 33 | printf("Failed to enable proton for app %d!\n", appId->getShortAppId()); 34 | return EXIT_FAILURE; 35 | } 36 | 37 | // Remove Shortcut 38 | if (!steam::api::removeGameShortcut(users.front(), *appId)) { 39 | printf("Failed to remove game shortcut!\n"); 40 | return EXIT_FAILURE; 41 | } 42 | 43 | // Get Grid, Hero, Icon and Logo for a game from SteamGridDB 44 | steam::Net::init(); 45 | 46 | steam::api::SteamGridDBAPI api("ENTER YOUR API KEY HERE"); 47 | 48 | auto searchResult = api.search("Harry Potter").get(); 49 | if (searchResult.empty()) 50 | return EXIT_FAILURE; 51 | 52 | auto gridsResult = api.getGrids(searchResult[0].id).get(); 53 | auto heroesResult = api.getHeroes(searchResult[0].id).get(); 54 | auto iconsResult = api.getIcons(searchResult[0].id).get(); 55 | auto logosResult = api.getLogos(searchResult[0].id).get(); 56 | 57 | if (!gridsResult.empty()) 58 | fmt::print("Grid: {}\n", gridsResult[0].url); 59 | if (!heroesResult.empty()) 60 | fmt::print("Hero: {}\n", heroesResult[0].url); 61 | if (!iconsResult.empty()) 62 | fmt::print("Icon: {}\n", iconsResult[0].url); 63 | if (!logosResult.empty()) 64 | fmt::print("Logo: {}\n", logosResult[0].url); 65 | 66 | steam::Net::exit(); 67 | 68 | // Restart Steam 69 | steam::api::restartSteam(); 70 | 71 | return EXIT_SUCCESS; 72 | } --------------------------------------------------------------------------------