'OHRRPGCE GAME - code for interfacing with Steamworks '(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. ' #include "common_base.bi" #include "steam.bi" #include "steam_internal.bi" ' #define DEBUG_STEAM #ifndef __FB_BLACKBOX__ 'We dynamically link to the steamworks library so that a single build can be used 'for both Steam and non-Steam games with the library being optional. 'You can comment this out to link to libsteam_api normally (after adding it and "-L ." to SConscript). 'Blackbox also emulates the Steam API for achievements, everything else is a stub. #define RUNTIME_LINK #endif namespace Steam #define steam_error(msg) debug "steam: " msg #ifdef DEBUG_STEAM #define steam_debug(msg) debug "steam: " msg #else #define steam_debug(msg) #endif ' event handlers declare sub OnUserStatsReceived(msg as UserStatsReceived_t ptr) ' static variables dim shared steam_user_stats as ISteamUserStats ptr dim shared steam_friends as ISteamFriends ptr #ifdef RUNTIME_LINK dim shared steamworks_handle as any ptr = null ' decl_sub/decl_function declare function pointers which are loaded by load_libsteam_api. ' (args includes the arg list and the function return type too) #define decl_sub(name, args) dim shared name as sub args #define decl_function(name, args) dim shared name as function args #else dim shared is_initialized as boolean = false ' Normal declarations #define decl_sub(name, args) declare sub name args #define decl_function(name, args) declare function name args #endif extern "C" ' basic init/deinit decl_function (SteamAPI_Init, () as boolean) decl_sub (SteamAPI_Shutdown, ()) decl_function (SteamAPI_RestartAppIfNecessary, (byval unOwnAppID as uinteger) as boolean) ' callback infrastructure, for run_frame decl_function (SteamAPI_GetHSteamPipe, () as HSteamPipe) decl_sub (SteamAPI_ManualDispatch_Init, ()) decl_sub (SteamAPI_ManualDispatch_RunFrame, (byval hSteamPipe as HSteamPipe)) decl_function (SteamAPI_ManualDispatch_GetNextCallback, (hSteamPipe as HSteamPipe, pCallbackMsg as CallbackMsg_t ptr) as boolean) decl_sub (SteamAPI_ManualDispatch_FreeLastCallback, (hSteamPipe as HSteamPipe)) decl_function (SteamAPI_ManualDispatch_GetAPICallResult, (hSteamPipe as HSteamPipe, hSteamAPICall as SteamAPICall_t, pCallback as any ptr, cubCallback as integer, iCallbackExpected as integer, pbFailed as boolean ptr) as boolean) ' achievements decl_function (SteamAPI_SteamUserStats_v012, () as ISteamUserStats ptr) 'Only in Steamworks SDK v1.53+, so won't use it to support older libs packaged with some OHR games 'decl_function (SteamAPI_SteamUserStats, () as ISteamUserStats ptr) decl_function (SteamAPI_ISteamUserStats_RequestCurrentStats, (byval self as ISteamUserStats ptr) as boolean) decl_function (SteamAPI_ISteamUserStats_SetAchievement, (byval self as ISteamUserStats ptr, byval name as const zstring ptr) as boolean) decl_function (SteamAPI_ISteamUserStats_ClearAchievement, (byval self as ISteamUserStats ptr, byval name as const zstring ptr) as boolean) decl_function (SteamAPI_ISteamUserStats_StoreStats, (byval self as ISteamUserStats ptr) as boolean) decl_function (SteamAPI_ISteamUserStats_IndicateAchievementProgress, (byval self as ISteamUserStats ptr, byval name as const zstring ptr, progress as uinteger, max_progress as uinteger) as boolean) ' friends decl_function (SteamAPI_SteamFriends_v017, () as ISteamFriends ptr) decl_function (SteamAPI_ISteamFriends_SetRichPresence, (byval self as ISteamFriends ptr, byval pchKey as const zstring ptr, byval pchValue as const zstring ptr) as boolean) end extern #ifdef RUNTIME_LINK #macro MUSTLOAD(hfile, procedure) procedure = dylibsymbol(hfile, #procedure) if procedure = NULL then steam_error("Unable to find " & #procedure) return false end if #endmacro 'On failure returns false, and the caller needs to dylibfree(steamworks_handle) function load_libsteam_api() as boolean #ifdef __FB_WIN32__ #ifdef __FB_64BIT__ 'Future, although not yet supported #define STEAM_LIB "steam_api64" #define STEAM_FULL_FNAME "steam_api64.dll" #else #define STEAM_LIB "steam_api" #define STEAM_FULL_FNAME "steam_api.dll" #endif #elseif defined(__FB_DARWIN__) #define STEAM_LIB "steam_api" #define STEAM_FULL_FNAME "libsteam_api.dylib" #else 'Either Linux or maybe a BSD (on which Steam isn't officially support but can be run) #define STEAM_LIB "steam_api" #define STEAM_FULL_FNAME "libsteam_api.so" #endif steamworks_handle = dylibload(STEAM_LIB) if steamworks_handle = null then debuginfo("Running without Steam, unable to load " STEAM_FULL_FNAME) return false end if MUSTLOAD(steamworks_handle, SteamAPI_Init) MUSTLOAD(steamworks_handle, SteamAPI_Shutdown) MUSTLOAD(steamworks_handle, SteamAPI_RestartAppIfNecessary) MUSTLOAD(steamworks_handle, SteamAPI_GetHSteamPipe) MUSTLOAD(steamworks_handle, SteamAPI_ManualDispatch_Init) MUSTLOAD(steamworks_handle, SteamAPI_ManualDispatch_RunFrame) MUSTLOAD(steamworks_handle, SteamAPI_ManualDispatch_GetNextCallback) MUSTLOAD(steamworks_handle, SteamAPI_ManualDispatch_FreeLastCallback) MUSTLOAD(steamworks_handle, SteamAPI_ManualDispatch_GetAPICallResult) MUSTLOAD(steamworks_handle, SteamAPI_SteamUserStats_v012) MUSTLOAD(steamworks_handle, SteamAPI_ISteamUserStats_RequestCurrentStats) MUSTLOAD(steamworks_handle, SteamAPI_ISteamUserStats_SetAchievement) MUSTLOAD(steamworks_handle, SteamAPI_ISteamUserStats_ClearAchievement) MUSTLOAD(steamworks_handle, SteamAPI_ISteamUserStats_StoreStats) MUSTLOAD(steamworks_handle, SteamAPI_ISteamUserStats_IndicateAchievementProgress) MUSTLOAD(steamworks_handle, SteamAPI_SteamFriends_v017) MUSTLOAD(steamworks_handle, SteamAPI_ISteamFriends_SetRichPresence) return true end function #endif ' #ifdef RUNTIME_LINK function initialize() as boolean #ifdef RUNTIME_LINK if load_libsteam_api() = false then uninitialize() return false end if #else is_initialized = true #endif if SteamAPI_Init() = false then steam_error("Unable to initialize Steamworks, Steam not running or missing steam_appid.txt?") uninitialize() return false end if ' This is necessary only if steam_appid.txt file doesn't exist, and not launched via Steam ' if SteamAPI_RestartAppIfNecessary( ourAppId ) <> false then ' steam_debug("Steam asks to restart the application") ' exit_gracefully() ' end if SteamAPI_ManualDispatch_Init() ' all stuff to do with stats and achievements go through SteamUserStats interface: steam_user_stats = SteamAPI_SteamUserStats_v012() if steam_user_stats = null then steam_error("Unable to obtain user stats object") uninitialize() return false else ' we need to instruct steam to fetch the user stats, so we can reward achievements later if SteamAPI_ISteamUserStats_RequestCurrentStats(steam_user_stats) = false then steam_error("Unable to request current stats") end if end if 'Friends API is used only for rich presence, which blackbox provides separately #ifndef __FB_BLACKBOX__ steam_friends = SteamAPI_SteamFriends_v017() if steam_friends = null then steam_error("Unable to obtain friends object") uninitialize() return false end if #endif debuginfo "Steam initialized" return true end function sub uninitialize() #ifdef RUNTIME_LINK if steamworks_handle <> null then dylibfree(steamworks_handle) steamworks_handle = null end if #else is_initialized = false #endif end sub function available() as boolean #ifdef RUNTIME_LINK return steamworks_handle <> null #else return is_initialized #endif end function sub reward_achievement(id as const string) if available() = false then return if SteamAPI_ISteamUserStats_SetAchievement(steam_user_stats, id) = false then steam_error("Unable to reward achievement: " & id) else if SteamAPI_ISteamUserStats_StoreStats(steam_user_stats) = false then steam_error("Unable to persist stats") end if end if end sub sub clear_achievement(id as string) if available() = false then return if SteamAPI_ISteamUserStats_ClearAchievement(steam_user_stats, id) = false then steam_error("Unable to clear an achievement: " & id) end if end sub sub notify_achievement_progress(id as const string, progress as integer, max_progress as integer) if available() = false then return if SteamAPI_ISteamUserStats_IndicateAchievementProgress(steam_user_stats, id, progress, max_progress) = false then steam_error("Unable to indicate achievement progress: " & id) end if end sub #ifndef __FB_BLACKBOX__ 'Blackbox has its own version of this (blackbox_set_rich_presence()) 'Set the current status string shown by a user in the friends list. 'The first value is actually the name of a rich presence localization token. A list of these 'strings must be uploaded to Steamworks (for at least English). They can contain '%subvalue%' 'which is substituted with *substitution. 'Blackbox works the same, with extern lists of token values, except '%s' is substituted. sub set_rich_presence(token_id as const zstring ptr, substitution as const zstring ptr) if available() = false then return dim tokenname as string = "#" & *token_id if SteamAPI_ISteamFriends_SetRichPresence(steam_friends, "steam_display", tokenname) = false then steam_error("Unabled to set steam_display rich presence") end if if SteamAPI_ISteamFriends_SetRichPresence(steam_friends, "subvalue", substitution) = false then steam_error("Unabled to set subvalue rich presence") end if end sub #endif #macro CALLBACK_HANDLER(typ, handler) case typ.k_iCallback steam_debug(#typ ", message length: " & callback.m_cubParam) dim typ##Msg as typ ptr = cast(typ ptr, callback.m_pubParam) handler(typ##Msg) #endmacro #macro IGNORE(id) case id ' steam_debug("Ignored Steam id: " & id) #endmacro dim shared achieve_timer as integer = -1 sub run_frame() #ifndef __FB_BLACKBOX__ if available() = false then return ' steam_debug("run_steam_frame") dim hSteamPipe as HSteamPipe = SteamAPI_GetHSteamPipe() SteamAPI_ManualDispatch_RunFrame(hSteamPipe) dim callback as CallbackMsg_t while SteamAPI_ManualDispatch_GetNextCallback(hSteamPipe, @callback) ' Check for dispatching API call results if callback.m_iCallback = 703 then dim pCallCompleted as SteamAPICallCompleted_t ptr = cast(SteamAPICallCompleted_t ptr, @callback) dim pTmpCallResult as any ptr = allocate(pCallCompleted->m_cubParam) dim bFailed as boolean if SteamAPI_ManualDispatch_GetAPICallResult ( hSteamPipe, pCallCompleted->m_hAsyncCall, pTmpCallResult, pCallCompleted->m_cubParam, pCallCompleted->m_iCallback, @bFailed ) then ' Dispatch the call result to the registered handler(s) for the ' call identified by pCallCompleted->m_hAsyncCall steam_debug("Call Completed handler") end if deallocate(pTmpCallResult) else ' Look at callback.m_iCallback to see what kind of callback it is, ' and dispatch to appropriate handler(s) select case callback.m_iCallback CALLBACK_HANDLER(UserStatsReceived_t, OnUserStatsReceived) ' these are messages that we either don't know the identity of, or we don't care IGNORE(715) IGNORE(304) IGNORE(711) IGNORE(903) IGNORE(501) IGNORE(502) ' favorites list changed IGNORE(1006) IGNORE(1102) ' user stats stored case else steam_debug("Some other handler: " & callback.m_iCallback) end select end if SteamAPI_ManualDispatch_FreeLastCallback(hSteamPipe) wend #endif end sub private sub OnUserStatsReceived(msg as UserStatsReceived_t ptr) steam_debug("On User Stats Received") ' unsure if we actually need to do anything in response to this. ' TODO: buffer any achievement activity that happens before this call? ' achieve_timer = 1000 end sub end namespace