├── .gitignore ├── LICENSE ├── readme.md └── steam_user_stats_lite.rb /.gitignore: -------------------------------------------------------------------------------- 1 | TestProj 2 | steam_api.dll 3 | steam_api64.dll 4 | steam_appid.txt 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, 2016, 2020 Yukai Li 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Someone I know is working on an RPG Maker game, and wanted to add achievements without the hassle of writing lots of C++ code. Good news is as of Steamworks SDK 1.32, exports have been added so C++ is not necessary to use most of Steamworks. As such, I have created a script that requires no DLLs other than the Steamworks DLL itself. 2 | 3 | Here's how to use the script: 4 | 5 | * Copy steam_api.dll from the Steamworks SDK to your project root folder. **Important: you must use Steamworks SDK 1.48 or above for r7!** Check tagged revisions if you need a version for an older SDK version. 6 | * Paste the script into the script editor. 7 | * In an event script or wherever you need it, write the following: 8 | 9 | steam = SteamUserStatsLite.instance 10 | steam.set_achievement 'YOUR_ACH_ID_HERE' 11 | steam.update 12 | 13 | * If that second line returns true, the achievement has been set. Otherwise, you'll want to check if you set up achievements properly on Steam. 14 | 15 | Please go to https://gmman.github.io/RGSS_SteamUserStatsLite for documentation. 16 | -------------------------------------------------------------------------------- /steam_user_stats_lite.rb: -------------------------------------------------------------------------------- 1 | # cyanic's Quick and Easy Steamworks Achievements Integration for Ruby 2 | # https://github.com/GMMan/RGSS_SteamUserStatsLite 3 | # r7 02/25/20 4 | # 5 | # Copyright (c) 2015, 2016, 2020 Yukai Li 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # 26 | # Drop steam_api.dll into the root of your project. Requires Steamworks SDK version >= 1.48. 27 | # 28 | # "Miller complained about how hard achievements were to implement in C++, so this was born." 29 | # 30 | 31 | $imported ||= {} 32 | $imported['cyanic-SteamUserStatsLite'] = 7 # Slightly unorthodox, it's a version number. 33 | 34 | # A context class to get Steamworks pointers to interfaces. 35 | # 36 | # @author cyanic 37 | class SteamAPIContext 38 | STEAMUSERSTATS_INTERFACE_VERSION = 'STEAMUSERSTATS_INTERFACE_VERSION011' 39 | STEAMAPPS_INTERFACE_VERSION = 'STEAMAPPS_INTERFACE_VERSION008' 40 | 41 | # Instantiates a new instance of +SteamAPIContext+. 42 | def initialize 43 | @initted = false 44 | @h_steam_user = @@dll_SteamAPI_GetHSteamUser.call 45 | return if (@steam_user_stats = @@dll_SteamInternal_FindOrCreateUserInterface.call(@h_steam_user, STEAMUSERSTATS_INTERFACE_VERSION)) == 0 46 | return if (@steam_apps = @@dll_SteamInternal_FindOrCreateUserInterface.call(@h_steam_user, STEAMAPPS_INTERFACE_VERSION)) == 0 47 | 48 | @initted = true 49 | end 50 | 51 | # Checks if context is initialized. 52 | # 53 | # @return [true, false] Whether context is initialized. 54 | def initted? 55 | @initted 56 | end 57 | 58 | # Gets the ISteamUserStats pointer 59 | # 60 | # @return [Fixnum, nil] The ISteamUserStats pointer if context is initialized, otherwise +nil+. 61 | def steam_user_stats 62 | @steam_user_stats if initted? 63 | end 64 | 65 | # Gets the ISteamApps pointer 66 | # 67 | # @return [Fixnum, nil] The ISteamUserStats pointer if context is initialized, otherwise +nil+. 68 | def steam_apps 69 | @steam_apps if initted? 70 | end 71 | 72 | private 73 | def self.is_64bit? 74 | # Probably very bad detection of whether current runtime is 64-bit 75 | (/x64/ =~ RUBY_PLATFORM) != nil 76 | end 77 | 78 | def self.steam_dll_name 79 | @@dll_name ||= self.is_64bit? ? 'steam_api64' : 'steam_api' 80 | end 81 | 82 | @@dll_SteamAPI_GetHSteamUser = Win32API.new(self.steam_dll_name, 'SteamAPI_GetHSteamUser', '', 'I') 83 | @@dll_SteamInternal_FindOrCreateUserInterface = Win32API.new(self.steam_dll_name, 'SteamInternal_FindOrCreateUserInterface', 'IP', 'I') 84 | 85 | end 86 | 87 | # A simple class for Steamworks UserStats integration. 88 | # 89 | # @author cyanic 90 | class SteamUserStatsLite 91 | 92 | # Instantiates a new instance of +SteamUserStatsLite+. 93 | def initialize 94 | @initted = false 95 | api_initted = @@dll_SteamAPI_Init.call % 256 != 0 96 | if api_initted 97 | @context = SteamAPIContext.new 98 | if @context.initted? 99 | @i_apps = @context.steam_apps 100 | @i_user_stats = @context.steam_user_stats 101 | @initted = true 102 | self.request_current_stats 103 | self.update 104 | end 105 | end 106 | end 107 | 108 | # Shuts down Steamworks. 109 | # 110 | # @return [void] 111 | def shutdown 112 | if @initted 113 | @i_apps = nil 114 | @i_user_stats = nil 115 | @@dll_SteamAPI_Shutdown.call 116 | @initted = false 117 | end 118 | end 119 | 120 | # Checks if Steamworks is initialized. 121 | # 122 | # @return [true, false] Whether Steamworks is initialized. 123 | def initted? 124 | @initted 125 | end 126 | 127 | # Restarts the app if Steamworks is not availble. 128 | # 129 | # @param app_id [Integer] The app ID to relaunch as. 130 | # @return [true, false] +true+ if current instance should exit, +false+ if not. 131 | def self.restart_app_if_necessary(app_id) 132 | @@dll_SteamAPI_RestartAppIfNecessary.call(app_id) % 256 != 0 133 | end 134 | 135 | # Runs Steam callbacks. 136 | # 137 | # @return [void] 138 | def update 139 | @@dll_SteamAPI_RunCallbacks.call if initted? 140 | end 141 | 142 | # Checks if current app is owned. 143 | # 144 | # @return [true, false, nil] Whether the current user has a license for the current app. +nil+ is returned if ownership status can't be retrieved. 145 | def is_subscribed 146 | if initted? 147 | @@dll_SteamAPI_ISteamApps_BIsSubscribed.call(@i_apps) % 256 != 0 148 | else 149 | nil 150 | end 151 | end 152 | 153 | # Checks if a DLC is installed. 154 | # 155 | # @param app_id [Integer] The app ID of the DLC to check. 156 | # @return [true, false, nil] Whether the DLC is installed. +nil+ is returned if the installation status can't be retrieved. 157 | def is_dlc_installed(app_id) 158 | if initted? 159 | @@dll_SteamAPI_ISteamApps_BIsDlcInstalled.call(@i_apps, app_id) % 256 != 0 160 | else 161 | nil 162 | end 163 | end 164 | 165 | # Pulls current user's stats from Steam. 166 | # 167 | # @return [true, false] Whether the stats have been successfully pulled. 168 | def request_current_stats 169 | if initted? 170 | @@dll_SteamAPI_ISteamUserStats_RequestCurrentStats.call(@i_user_stats) % 256 != 0 171 | else 172 | false 173 | end 174 | end 175 | 176 | # Gets the value of an INT stat. 177 | # 178 | # @param name [String] The name of the stat. 179 | # @return [Integer, nil] The value of the stat, or +nil+ if the stat cannot be retrieved. 180 | def get_stat_int(name) 181 | if initted? 182 | val = ' ' * 4 183 | ok = @@dll_SteamAPI_ISteamUserStats_GetStatInt32.call(@i_user_stats, name, val) % 256 != 0 184 | ok ? val.unpack('I')[0] : nil 185 | else 186 | nil 187 | end 188 | end 189 | 190 | # Gets the value of an FLOAT stat. 191 | # 192 | # @param name [String] The name of the stat. 193 | # @return [Float, nil] The value of the stat, or +nil+ if the stat cannot be retrieved. 194 | def get_stat_float(name) 195 | if initted? 196 | val = ' ' * 4 197 | ok = @@dll_SteamAPI_ISteamUserStats_GetStatFloat.call(@i_user_stats, name, val) % 256 != 0 198 | ok ? val.unpack('f')[0] : nil 199 | else 200 | nil 201 | end 202 | end 203 | 204 | # Sets the value of a stat. 205 | # 206 | # @param name [String] The name of the stat. 207 | # @param val [Integer, Float] The value of the stat. 208 | # @return [true, false] Whether the stat was successfully updated. 209 | # @example 210 | # steam = SteamUserStatsLite.instance 211 | # steam.set_stat 'YOUR_STAT_ID_HERE', 100 212 | # steam.update 213 | def set_stat(name, val) 214 | if initted? 215 | if val.is_a? Float 216 | ok = @@dll_SteamAPI_ISteamUserStats_SetStatFloat.call(@i_user_stats, name, self.class.pack_float(val)) % 256 != 0 217 | else 218 | ok = @@dll_SteamAPI_ISteamUserStats_SetStatInt32.call(@i_user_stats, name, val.to_i) % 256 != 0 219 | end 220 | @@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok 221 | else 222 | false 223 | end 224 | end 225 | 226 | # Updates an AVGRATE stat. 227 | # 228 | # @param name [String] The name of the stat. 229 | # @param count_this_session [Float] The value during this session. 230 | # @param session_length [Float] The length of this session. 231 | # @return [true, false] Whether the stat was successfully updated. 232 | def update_avg_rate_stat(name, count_this_session, session_length) 233 | if initted? 234 | packed = self.class.pack_double session_length 235 | ok = @@dll_SteamAPI_ISteamUserStats_UpdateAvgRateStat.call(@i_user_stats, name, self.class.pack_float(count_this_session.to_f), packed[0], packed[1]) % 256 != 0 236 | @@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok 237 | else 238 | false 239 | end 240 | end 241 | 242 | # Gets an achievement's state. 243 | # 244 | # @param name [String] The name of the achievement. 245 | # @return [true, false, nil] Whether the achievement has unlocked, or +nil+ if the achievement cannot be retrieved. 246 | def get_achievement(name) 247 | if initted? 248 | val = ' ' 249 | ok = @@dll_SteamAPI_ISteamUserStats_GetAchievement.call(@i_user_stats, name, val) % 256 != 0 250 | ok ? val.unpack('C')[0] != 0 : nil 251 | else 252 | nil 253 | end 254 | end 255 | 256 | # Sets an achievement as unlocked. 257 | # 258 | # @param name [String] The name of the achievement. 259 | # @return [true, false] Whether the achievement was set successfully. 260 | # @example 261 | # steam = SteamUserStatsLite.instance 262 | # steam.set_achievement 'YOUR_ACH_ID_HERE' 263 | # steam.update 264 | def set_achievement(name) 265 | if initted? 266 | ok = @@dll_SteamAPI_ISteamUserStats_SetAchievement.call(@i_user_stats, name) % 256 != 0 267 | @@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok 268 | else 269 | false 270 | end 271 | end 272 | 273 | # Sets an achievement as locked. 274 | # 275 | # @param name [String] The name of the achievement. 276 | # @return [true, false] Whether the achievement was cleared successfully. 277 | def clear_achievement(name) 278 | if initted? 279 | ok = @@dll_SteamAPI_ISteamUserStats_ClearAchievement.call(@i_user_stats, name) % 256 != 0 280 | @@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok 281 | else 282 | false 283 | end 284 | end 285 | 286 | # Gets an achievement's state and unlock time. 287 | # 288 | # @param name [String] The name of the achievement. 289 | # @return [] The achievement's state (+true+ or +false+) and the time it was unlocked. 290 | def get_achievement_and_unlock_time(name) 291 | if initted? 292 | achieved = ' ' 293 | unlock_time = ' ' * 4 294 | ok = @@dll_SteamAPI_ISteamUserStats_GetAchievementAndUnlockTime.call(@i_user_stats, name, achieved, unlock_time) % 256 != 0 295 | ok ? [achieved.unpack('C')[0] != 0, Time.at(unlock_time.unpack('L')[0])] : nil 296 | else 297 | nil 298 | end 299 | end 300 | 301 | # Gets the value of an achievement's display attribute. 302 | # 303 | # @param name [String] The name of the achievement. 304 | # @param key [String] The key of the display attribute. 305 | # @return [String] The value of the display attribute. 306 | def get_achievement_display_attribute(name, key) 307 | if initted? 308 | @@dll_SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute.call @i_user_stats, name, key 309 | else 310 | nil 311 | end 312 | end 313 | 314 | # Updates achievement progress 315 | # 316 | # @param name [String] The name of the achievement. 317 | # @param cur_progress [Integer] The current progress of the achievement. 318 | # @param max_progress [Integer] The maximum progress of the achievement. 319 | # @return [true, false] Whether the achievement progress was updated. 320 | def indicate_achievement_progress(name, cur_progress, max_progress) 321 | if initted? 322 | ok = @@dll_SteamAPI_ISteamUserStats_IndicateAchievementProgress.call(@i_user_stats, name, cur_progress.to_i, max_progress.to_i) % 256 != 0 323 | @@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok 324 | else 325 | false 326 | end 327 | end 328 | 329 | # Gets the number of achievements. 330 | # 331 | # @return [Integer, nil] The number of achievements, or +nil+ if the number cannot be retrieved. 332 | def get_num_achievements 333 | if initted? 334 | @@dll_SteamAPI_ISteamUserStats_GetNumAchievements.call @i_user_stats 335 | else 336 | nil 337 | end 338 | end 339 | 340 | # Gets the name of an achievement by its index. 341 | # 342 | # @param achievement [Integer] The index of the achievement. 343 | # @return [String] The name of the achievement. 344 | def get_achievement_name(achievement) 345 | if initted? 346 | @@dll_SteamAPI_ISteamUserStats_GetAchievementName.call @i_user_stats, achievement 347 | else 348 | nil 349 | end 350 | end 351 | 352 | # Resets all stats. 353 | # 354 | # @param achievements_too [true, false] Whether to reset achievements as well. 355 | # @return [true, false] Whether achievements have been reset. 356 | def reset_all_stats(achievements_too) 357 | if initted? 358 | ok = @@dll_SteamAPI_ISteamUserStats_ResetAllStats.call(@i_user_stats, achievements_too ? 1 : 0) % 256 != 0 359 | @@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok 360 | else 361 | false 362 | end 363 | end 364 | 365 | # Gets the global instance of SteamUserStatsLite. 366 | # 367 | # @return [SteamUserStatsLite] The global instance of the class. 368 | def self.instance 369 | @@instance 370 | end 371 | 372 | private 373 | def self.is_64bit? 374 | # Probably very bad detection of whether current runtime is 64-bit 375 | (/x64/ =~ RUBY_PLATFORM) != nil 376 | end 377 | 378 | def self.steam_dll_name 379 | @@dll_name ||= self.is_64bit? ? 'steam_api64' : 'steam_api' 380 | end 381 | 382 | # Function imports 383 | @@dll_SteamAPI_RestartAppIfNecessary = Win32API.new(self.steam_dll_name, 'SteamAPI_RestartAppIfNecessary', 'I', 'I') 384 | @@dll_SteamAPI_Init = Win32API.new(self.steam_dll_name, 'SteamAPI_Init', '', 'I') 385 | @@dll_SteamAPI_Shutdown = Win32API.new(self.steam_dll_name, 'SteamAPI_Shutdown', '', 'V') 386 | @@dll_SteamAPI_RunCallbacks = Win32API.new(self.steam_dll_name, 'SteamAPI_RunCallbacks', '', 'V') 387 | @@dll_SteamAPI_ISteamUserStats_RequestCurrentStats = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_RequestCurrentStats', 'P', 'I') 388 | @@dll_SteamAPI_ISteamUserStats_GetStatInt32 = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetStatInt32', 'PPP', 'I') 389 | @@dll_SteamAPI_ISteamUserStats_GetStatFloat = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetStatFloat', 'PPP', 'I') 390 | @@dll_SteamAPI_ISteamUserStats_SetStatInt32 = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_SetStatInt32', 'PPL', 'I') 391 | @@dll_SteamAPI_ISteamUserStats_SetStatFloat = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_SetStatFloat', 'PPI', 'I') 392 | @@dll_SteamAPI_ISteamUserStats_UpdateAvgRateStat = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_UpdateAvgRateStat', 'PPIII', 'I') 393 | @@dll_SteamAPI_ISteamUserStats_GetAchievement = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievement', 'PPP', 'I') 394 | @@dll_SteamAPI_ISteamUserStats_SetAchievement = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_SetAchievement', 'PP', 'I') 395 | @@dll_SteamAPI_ISteamUserStats_ClearAchievement = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_ClearAchievement', 'PP', 'I') 396 | @@dll_SteamAPI_ISteamUserStats_GetAchievementAndUnlockTime = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievementAndUnlockTime', 'PPPP', 'I') 397 | @@dll_SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute', 'PPP', 'P') 398 | @@dll_SteamAPI_ISteamUserStats_IndicateAchievementProgress = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_IndicateAchievementProgress', 'PPII', 'I') 399 | @@dll_SteamAPI_ISteamUserStats_GetNumAchievements = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetNumAchievements', 'P', 'I') 400 | @@dll_SteamAPI_ISteamUserStats_GetAchievementName = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievementName', 'PI', 'P') 401 | @@dll_SteamAPI_ISteamUserStats_StoreStats = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_StoreStats', 'P', 'I') 402 | @@dll_SteamAPI_ISteamUserStats_ResetAllStats = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_ResetAllStats', 'PI', 'I') 403 | @@dll_SteamAPI_ISteamApps_BIsSubscribed = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamApps_BIsSubscribed', 'P', 'I') 404 | @@dll_SteamAPI_ISteamApps_BIsDlcInstalled = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamApps_BIsDlcInstalled', 'PI', 'I') 405 | 406 | @@instance = self.new 407 | 408 | def self.pack_float(val) 409 | # Packs number to a string, then unpack to an int 410 | inter = [val].pack 'e' 411 | inter.unpack('I')[0] 412 | end 413 | 414 | def self.pack_double(val) 415 | # Packs number to a string, then unpack to an array of two ints 416 | inter = [val].pack 'd' 417 | inter.unpack 'II' 418 | end 419 | 420 | end 421 | --------------------------------------------------------------------------------