Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
# cyanic's Quick and Easy Steamworks Achievements Integration for Ruby
# https://github.com/GMMan/RGSS_SteamUserStatsLite
# r7 02/25/20
#
# Copyright (c) 2015, 2016, 2020 Yukai Li
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#
# Drop steam_api.dll into the root of your project. Requires Steamworks SDK version >= 1.48.
#
# "Miller complained about how hard achievements were to implement in C++, so this was born."
#
$imported ||= {}
$imported['cyanic-SteamUserStatsLite'] = 7 # Slightly unorthodox, it's a version number.
# A context class to get Steamworks pointers to interfaces.
#
# @author cyanic
class SteamAPIContext
STEAMUSERSTATS_INTERFACE_VERSION = 'STEAMUSERSTATS_INTERFACE_VERSION011'
STEAMAPPS_INTERFACE_VERSION = 'STEAMAPPS_INTERFACE_VERSION008'
# Instantiates a new instance of +SteamAPIContext+.
def initialize
@initted = false
@h_steam_user = @@dll_SteamAPI_GetHSteamUser.call
return if (@steam_user_stats = @@dll_SteamInternal_FindOrCreateUserInterface.call(@h_steam_user, STEAMUSERSTATS_INTERFACE_VERSION)) == 0
return if (@steam_apps = @@dll_SteamInternal_FindOrCreateUserInterface.call(@h_steam_user, STEAMAPPS_INTERFACE_VERSION)) == 0
@initted = true
end
# Checks if context is initialized.
#
# @return [true, false] Whether context is initialized.
def initted?
@initted
end
# Gets the ISteamUserStats pointer
#
# @return [Fixnum, nil] The ISteamUserStats pointer if context is initialized, otherwise +nil+.
def steam_user_stats
@steam_user_stats if initted?
end
# Gets the ISteamApps pointer
#
# @return [Fixnum, nil] The ISteamUserStats pointer if context is initialized, otherwise +nil+.
def steam_apps
@steam_apps if initted?
end
private
def self.is_64bit?
# Probably very bad detection of whether current runtime is 64-bit
(/x64/ =~ RUBY_PLATFORM) != nil
end
def self.steam_dll_name
@@dll_name ||= self.is_64bit? ? 'steam_api64' : 'steam_api'
end
@@dll_SteamAPI_GetHSteamUser = Win32API.new(self.steam_dll_name, 'SteamAPI_GetHSteamUser', '', 'I')
@@dll_SteamInternal_FindOrCreateUserInterface = Win32API.new(self.steam_dll_name, 'SteamInternal_FindOrCreateUserInterface', 'IP', 'I')
end
# A simple class for Steamworks UserStats integration.
#
# @author cyanic
class SteamUserStatsLite
# Instantiates a new instance of +SteamUserStatsLite+.
def initialize
@initted = false
api_initted = @@dll_SteamAPI_Init.call % 256 != 0
if api_initted
@context = SteamAPIContext.new
if @context.initted?
@i_apps = @context.steam_apps
@i_user_stats = @context.steam_user_stats
@initted = true
self.request_current_stats
self.update
end
end
end
# Shuts down Steamworks.
#
# @return [void]
def shutdown
if @initted
@i_apps = nil
@i_user_stats = nil
@@dll_SteamAPI_Shutdown.call
@initted = false
end
end
# Checks if Steamworks is initialized.
#
# @return [true, false] Whether Steamworks is initialized.
def initted?
@initted
end
# Restarts the app if Steamworks is not availble.
#
# @param app_id [Integer] The app ID to relaunch as.
# @return [true, false] +true+ if current instance should exit, +false+ if not.
def self.restart_app_if_necessary(app_id)
@@dll_SteamAPI_RestartAppIfNecessary.call(app_id) % 256 != 0
end
# Runs Steam callbacks.
#
# @return [void]
def update
@@dll_SteamAPI_RunCallbacks.call if initted?
end
# Checks if current app is owned.
#
# @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.
def is_subscribed
if initted?
@@dll_SteamAPI_ISteamApps_BIsSubscribed.call(@i_apps) % 256 != 0
else
nil
end
end
# Checks if a DLC is installed.
#
# @param app_id [Integer] The app ID of the DLC to check.
# @return [true, false, nil] Whether the DLC is installed. +nil+ is returned if the installation status can't be retrieved.
def is_dlc_installed(app_id)
if initted?
@@dll_SteamAPI_ISteamApps_BIsDlcInstalled.call(@i_apps, app_id) % 256 != 0
else
nil
end
end
# Pulls current user's stats from Steam.
#
# @return [true, false] Whether the stats have been successfully pulled.
def request_current_stats
if initted?
@@dll_SteamAPI_ISteamUserStats_RequestCurrentStats.call(@i_user_stats) % 256 != 0
else
false
end
end
# Gets the value of an INT stat.
#
# @param name [String] The name of the stat.
# @return [Integer, nil] The value of the stat, or +nil+ if the stat cannot be retrieved.
def get_stat_int(name)
if initted?
val = ' ' * 4
ok = @@dll_SteamAPI_ISteamUserStats_GetStatInt32.call(@i_user_stats, name, val) % 256 != 0
ok ? val.unpack('I')[0] : nil
else
nil
end
end
# Gets the value of an FLOAT stat.
#
# @param name [String] The name of the stat.
# @return [Float, nil] The value of the stat, or +nil+ if the stat cannot be retrieved.
def get_stat_float(name)
if initted?
val = ' ' * 4
ok = @@dll_SteamAPI_ISteamUserStats_GetStatFloat.call(@i_user_stats, name, val) % 256 != 0
ok ? val.unpack('f')[0] : nil
else
nil
end
end
# Sets the value of a stat.
#
# @param name [String] The name of the stat.
# @param val [Integer, Float] The value of the stat.
# @return [true, false] Whether the stat was successfully updated.
# @example
# steam = SteamUserStatsLite.instance
# steam.set_stat 'YOUR_STAT_ID_HERE', 100
# steam.update
def set_stat(name, val)
if initted?
if val.is_a? Float
ok = @@dll_SteamAPI_ISteamUserStats_SetStatFloat.call(@i_user_stats, name, self.class.pack_float(val)) % 256 != 0
else
ok = @@dll_SteamAPI_ISteamUserStats_SetStatInt32.call(@i_user_stats, name, val.to_i) % 256 != 0
end
@@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok
else
false
end
end
# Updates an AVGRATE stat.
#
# @param name [String] The name of the stat.
# @param count_this_session [Float] The value during this session.
# @param session_length [Float] The length of this session.
# @return [true, false] Whether the stat was successfully updated.
def update_avg_rate_stat(name, count_this_session, session_length)
if initted?
packed = self.class.pack_double session_length
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
@@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok
else
false
end
end
# Gets an achievement's state.
#
# @param name [String] The name of the achievement.
# @return [true, false, nil] Whether the achievement has unlocked, or +nil+ if the achievement cannot be retrieved.
def get_achievement(name)
if initted?
val = ' '
ok = @@dll_SteamAPI_ISteamUserStats_GetAchievement.call(@i_user_stats, name, val) % 256 != 0
ok ? val.unpack('C')[0] != 0 : nil
else
nil
end
end
# Sets an achievement as unlocked.
#
# @param name [String] The name of the achievement.
# @return [true, false] Whether the achievement was set successfully.
# @example
# steam = SteamUserStatsLite.instance
# steam.set_achievement 'YOUR_ACH_ID_HERE'
# steam.update
def set_achievement(name)
if initted?
ok = @@dll_SteamAPI_ISteamUserStats_SetAchievement.call(@i_user_stats, name) % 256 != 0
@@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok
else
false
end
end
# Sets an achievement as locked.
#
# @param name [String] The name of the achievement.
# @return [true, false] Whether the achievement was cleared successfully.
def clear_achievement(name)
if initted?
ok = @@dll_SteamAPI_ISteamUserStats_ClearAchievement.call(@i_user_stats, name) % 256 != 0
@@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok
else
false
end
end
# Gets an achievement's state and unlock time.
#
# @param name [String] The name of the achievement.
# @return [<Object, Time>] The achievement's state (+true+ or +false+) and the time it was unlocked.
def get_achievement_and_unlock_time(name)
if initted?
achieved = ' '
unlock_time = ' ' * 4
ok = @@dll_SteamAPI_ISteamUserStats_GetAchievementAndUnlockTime.call(@i_user_stats, name, achieved, unlock_time) % 256 != 0
ok ? [achieved.unpack('C')[0] != 0, Time.at(unlock_time.unpack('L')[0])] : nil
else
nil
end
end
# Gets the value of an achievement's display attribute.
#
# @param name [String] The name of the achievement.
# @param key [String] The key of the display attribute.
# @return [String] The value of the display attribute.
def get_achievement_display_attribute(name, key)
if initted?
@@dll_SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute.call @i_user_stats, name, key
else
nil
end
end
# Updates achievement progress
#
# @param name [String] The name of the achievement.
# @param cur_progress [Integer] The current progress of the achievement.
# @param max_progress [Integer] The maximum progress of the achievement.
# @return [true, false] Whether the achievement progress was updated.
def indicate_achievement_progress(name, cur_progress, max_progress)
if initted?
ok = @@dll_SteamAPI_ISteamUserStats_IndicateAchievementProgress.call(@i_user_stats, name, cur_progress.to_i, max_progress.to_i) % 256 != 0
@@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok
else
false
end
end
# Gets the number of achievements.
#
# @return [Integer, nil] The number of achievements, or +nil+ if the number cannot be retrieved.
def get_num_achievements
if initted?
@@dll_SteamAPI_ISteamUserStats_GetNumAchievements.call @i_user_stats
else
nil
end
end
# Gets the name of an achievement by its index.
#
# @param achievement [Integer] The index of the achievement.
# @return [String] The name of the achievement.
def get_achievement_name(achievement)
if initted?
@@dll_SteamAPI_ISteamUserStats_GetAchievementName.call @i_user_stats, achievement
else
nil
end
end
# Resets all stats.
#
# @param achievements_too [true, false] Whether to reset achievements as well.
# @return [true, false] Whether achievements have been reset.
def reset_all_stats(achievements_too)
if initted?
ok = @@dll_SteamAPI_ISteamUserStats_ResetAllStats.call(@i_user_stats, achievements_too ? 1 : 0) % 256 != 0
@@dll_SteamAPI_ISteamUserStats_StoreStats.call(@i_user_stats) % 256 != 0 && ok
else
false
end
end
# Gets the global instance of SteamUserStatsLite.
#
# @return [SteamUserStatsLite] The global instance of the class.
def self.instance
@@instance
end
private
def self.is_64bit?
# Probably very bad detection of whether current runtime is 64-bit
(/x64/ =~ RUBY_PLATFORM) != nil
end
def self.steam_dll_name
@@dll_name ||= self.is_64bit? ? 'steam_api64' : 'steam_api'
end
# Function imports
@@dll_SteamAPI_RestartAppIfNecessary = Win32API.new(self.steam_dll_name, 'SteamAPI_RestartAppIfNecessary', 'I', 'I')
@@dll_SteamAPI_Init = Win32API.new(self.steam_dll_name, 'SteamAPI_Init', '', 'I')
@@dll_SteamAPI_Shutdown = Win32API.new(self.steam_dll_name, 'SteamAPI_Shutdown', '', 'V')
@@dll_SteamAPI_RunCallbacks = Win32API.new(self.steam_dll_name, 'SteamAPI_RunCallbacks', '', 'V')
@@dll_SteamAPI_ISteamUserStats_RequestCurrentStats = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_RequestCurrentStats', 'P', 'I')
@@dll_SteamAPI_ISteamUserStats_GetStatInt32 = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetStatInt32', 'PPP', 'I')
@@dll_SteamAPI_ISteamUserStats_GetStatFloat = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetStatFloat', 'PPP', 'I')
@@dll_SteamAPI_ISteamUserStats_SetStatInt32 = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_SetStatInt32', 'PPL', 'I')
@@dll_SteamAPI_ISteamUserStats_SetStatFloat = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_SetStatFloat', 'PPI', 'I')
@@dll_SteamAPI_ISteamUserStats_UpdateAvgRateStat = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_UpdateAvgRateStat', 'PPIII', 'I')
@@dll_SteamAPI_ISteamUserStats_GetAchievement = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievement', 'PPP', 'I')
@@dll_SteamAPI_ISteamUserStats_SetAchievement = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_SetAchievement', 'PP', 'I')
@@dll_SteamAPI_ISteamUserStats_ClearAchievement = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_ClearAchievement', 'PP', 'I')
@@dll_SteamAPI_ISteamUserStats_GetAchievementAndUnlockTime = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievementAndUnlockTime', 'PPPP', 'I')
@@dll_SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute', 'PPP', 'P')
@@dll_SteamAPI_ISteamUserStats_IndicateAchievementProgress = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_IndicateAchievementProgress', 'PPII', 'I')
@@dll_SteamAPI_ISteamUserStats_GetNumAchievements = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetNumAchievements', 'P', 'I')
@@dll_SteamAPI_ISteamUserStats_GetAchievementName = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_GetAchievementName', 'PI', 'P')
@@dll_SteamAPI_ISteamUserStats_StoreStats = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_StoreStats', 'P', 'I')
@@dll_SteamAPI_ISteamUserStats_ResetAllStats = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamUserStats_ResetAllStats', 'PI', 'I')
@@dll_SteamAPI_ISteamApps_BIsSubscribed = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamApps_BIsSubscribed', 'P', 'I')
@@dll_SteamAPI_ISteamApps_BIsDlcInstalled = Win32API.new(self.steam_dll_name, 'SteamAPI_ISteamApps_BIsDlcInstalled', 'PI', 'I')
@@instance = self.new
def self.pack_float(val)
# Packs number to a string, then unpack to an int
inter = [val].pack 'e'
inter.unpack('I')[0]
end
def self.pack_double(val)
# Packs number to a string, then unpack to an array of two ints
inter = [val].pack 'd'
inter.unpack 'II'
end
end