From 84141487ef18c23d54f097f68fec7157dcc4fe07 Mon Sep 17 00:00:00 2001 From: Riley Labrecque Date: Wed, 29 Jan 2014 12:56:10 +0200 Subject: [PATCH] v1.03 --- Readme.txt | 46 +++- SteamworksExample/GameEngine.cpp | 18 ++ SteamworksExample/GameEngine.h | 3 + SteamworksExample/Lobby.cpp | 13 +- SteamworksExample/Lobby.h | 3 +- SteamworksExample/Main.cpp | 3 +- SteamworksExample/MainMenu.cpp | 1 + SteamworksExample/RemoteStorage.cpp | 253 ++++++++++++++++++++ SteamworksExample/RemoteStorage.h | 59 +++++ SteamworksExample/SpaceWar.h | 1 + SteamworksExample/SpaceWarClient.cpp | 69 ++++-- SteamworksExample/SpaceWarClient.h | 20 +- SteamworksExample/SpaceWarServer.cpp | 2 +- SteamworksExample/SteamworksExample.vcproj | 12 +- public/steam/isteamclient.h | 5 + public/steam/isteamfriends.h | 43 +++- public/steam/isteamgameserver.h | 33 ++- public/steam/isteammatchmaking.h | 129 ++++++---- public/steam/isteamnetworking.h | 21 +- public/steam/isteamremotestorage.h | 45 ++++ public/steam/isteamuser.h | 9 +- public/steam/isteamuserstats.h | 23 +- public/steam/isteamutils.h | 33 +++ public/steam/matchmakingtypes.h | 2 + public/steam/steam_api.h | 100 +++++++- public/steam/steamclientpublic.h | 262 ++++++++------------- public/steam/steamtypes.h | 6 + redistributable_bin/SteamAPIUpdater.dll | Bin 1296880 -> 1296880 bytes redistributable_bin/UpdateDLLWrapper.dll | Bin 58616 -> 58616 bytes redistributable_bin/steam_api.dll | Bin 70392 -> 87288 bytes redistributable_bin/steam_api.lib | Bin 9100 -> 12538 bytes 31 files changed, 944 insertions(+), 270 deletions(-) create mode 100644 SteamworksExample/RemoteStorage.cpp create mode 100644 SteamworksExample/RemoteStorage.h create mode 100644 public/steam/isteamremotestorage.h diff --git a/Readme.txt b/Readme.txt index 5b1883d..0df5ec2 100644 --- a/Readme.txt +++ b/Readme.txt @@ -1,8 +1,8 @@ -This is the Steamworks SDK v1.02 +This is the Steamworks SDK v1.03 ================================================================ -Copyright © 1996-2008, Valve Corporation, All rights reserved. +Copyright © 1996-2009, Valve Corporation, All rights reserved. ================================================================ @@ -13,9 +13,47 @@ website at: http://partner.steamgames.com Revision History: +---------------------------------------------------------------- +v1.03 16th Jan 2009 +---------------------------------------------------------------- + +Major changes: +* ISteamRemoteStorage interface has been added, which contains functions to store per-user data in the Steam Cloud back-end. +** To use this, you must first use the partner web site to enable Cloud for your game. +** The current setting is allowing 1MB of storage per-game per-user (we hope to increase this over time). + +Lobby & Matchmaking related changes: +* ISteamFriends::GetFriendGamePlayed() now also return the steamID of the lobby the friend is in, if any. It now takes a pointer to a new FriendGameInfo_t struct, which it fills +* Removed ISteamFriends::GetFriendsLobbies(), since this is now redundant to ISteamFriends::GetFriendGamePlayed() +* Added enum ELobbyComparison, to set the comparison operator in ISteamMatchmaking::AddRequestLobbyListNumericalFilter() +* Changed ISteamMatchmaking::CreateLobby(), JoinLobby() and RequestLobbyList() to now return SteamAPICall_t handles, so you can easily track if a particular call has completed (see below) +* Added ISteamMatchmaking::SetLobbyType(), which can switch a lobby between searchable (public) and friends-only +* Added ISteamMatchmaking::GetLobbyOwner(), which returns the steamID of the user who is currently the owner of the lobby. The back-end ensures that one and only one user is ever the owner. If that user leaves the lobby, another user will become the owner. + +Steam game-overlay interaction: +* Added a new callback GameLobbyJoinRequested_t, which is sent to the game if the user selects 'Join friends game' from the Steam friends list, and that friend is in a lobby. The game should initiate connection to that lobby. +* Changed ISteamFriends::ActivateGameOverlay() can now go to "Friends", "Community", "Players", "Settings", "LobbyInvite", "OfficialGameGroup" +* Added ISteamFriends::ActivateGameOverlayToUser(), which can open a either a chat dialog or another users Steam community profile +* Added ISteamFriends::ActivateGameOverlayToWebPage(), which opens the Steam game-overlay web browser to the specified url + +Stats system changes: +* Added ISteamUserStats::RequestUserStats(), to download the current game stats of another user +* Added ISteamUserStats::GetUserStat() and ISteamUserStats::GetUserAchievement() to access the other users stats, once they've been downloaded + +Callback system changes: +* Added new method for handling asynchronous call results, currently used by CreateLobby(), JoinLobby(), RequestLobbyList(), and RequestUserStats(). Each of these functions returns a handle, SteamAPICall_t, that can be used to track the completion state of a call. +* Added new object CCallResult<>, which can map the completion of a SteamAPICall_t to a function, and include the right data. See SteamworksExample for a usage example. +* Added ISteamUtils::IsAPICallCompleted(), GetAPICallFailureReason(), and GetAPICallResult(), which can be used to track the state of a SteamAPICall_t (although it is recommended to use CCallResult<>, which wraps these up nicely) + +Other: +* Added ISteamGameServer::GetPublicIP(), which is the IP address of a game server as seen by the Steam back-end +* Added "allow relay" parameter to ISteamNetworking::CreateP2PConnectionSocket() and CreateListenSocket(), which specified if being bounced through Steam relay servers is OK if a direct p2p connection fails (will have a much higher latency, but increases chance of making a connection) +* Added IPCFailure_t callback, which will be posted to the game if Steam itself has crashed, or if Steam_RunCallbacks() hasn't been called in a long time + + ---------------------------------------------------------------- -v1.02 09/04/08 +v1.02 4th Sep 2008 ---------------------------------------------------------------- The following interfaces have been updated: @@ -90,7 +128,7 @@ This function now takes an eAvatarSize parameter, which can be k_EAvatarSize32x3 ---------------------------------------------------------------- -v1.01 08/08/08 +v1.01 8th Aug 2008 ---------------------------------------------------------------- The Steamworks SDK has been updated to simplfy game server authentication and better expose application state diff --git a/SteamworksExample/GameEngine.cpp b/SteamworksExample/GameEngine.cpp index 0b7205c..9fc9833 100644 --- a/SteamworksExample/GameEngine.cpp +++ b/SteamworksExample/GameEngine.cpp @@ -1591,6 +1591,24 @@ bool CGameEngine::BIsKeyDown( DWORD dwVK ) return false; } +//----------------------------------------------------------------------------- +// Purpose: Get a down key value +//----------------------------------------------------------------------------- +bool CGameEngine::BGetFirstKeyDown( DWORD *pdwVK ) +{ + std::set::iterator iter; + iter = m_SetKeysDown.begin(); + if ( iter != m_SetKeysDown.end() ) + { + *pdwVK = *iter; + return true; + } + else + { + return false; + } +} + //----------------------------------------------------------------------------- // Purpose: Find the engine instance tied to a given hwnd diff --git a/SteamworksExample/GameEngine.h b/SteamworksExample/GameEngine.h index 51018c6..bf605fb 100644 --- a/SteamworksExample/GameEngine.h +++ b/SteamworksExample/GameEngine.h @@ -154,6 +154,9 @@ public: // Get the current state of a key bool BIsKeyDown( DWORD dwVK ); + // Get the first (in some arbitrary order) key down, if any + bool BGetFirstKeyDown( DWORD *pdwVK ); + // Get current tick count for the game engine uint64 GetGameTickCount() { return m_ulGameTickCount; } diff --git a/SteamworksExample/Lobby.cpp b/SteamworksExample/Lobby.cpp index ea01c98..ed4fa38 100644 --- a/SteamworksExample/Lobby.cpp +++ b/SteamworksExample/Lobby.cpp @@ -207,7 +207,6 @@ public: // just initializes base data //----------------------------------------------------------------------------- CLobbyBrowser::CLobbyBrowser( CGameEngine *pGameEngine ) : - m_CallbackLobbyMatchList( this, &CLobbyBrowser::OnLobbyMatchListCallback ), m_CallbackLobbyDataUpdated( this, &CLobbyBrowser::OnLobbyDataUpdatedCallback ) { m_pGameEngine = pGameEngine; @@ -247,7 +246,9 @@ void CLobbyBrowser::Refresh() { m_bRequestingLobbies = true; // request all lobbies for this game - SteamMatchmaking()->RequestLobbyList(); + SteamAPICall_t hSteamAPICall = SteamMatchmaking()->RequestLobbyList(); + // set the function to call when this API call has completed + m_SteamCallResultLobbyMatchList.Set( hSteamAPICall, this, &CLobbyBrowser::OnLobbyMatchListCallback ); } } @@ -255,11 +256,17 @@ void CLobbyBrowser::Refresh() //----------------------------------------------------------------------------- // Purpose: Callback, on a list of lobbies being received from the Steam back-end //----------------------------------------------------------------------------- -void CLobbyBrowser::OnLobbyMatchListCallback( LobbyMatchList_t *pCallback ) +void CLobbyBrowser::OnLobbyMatchListCallback( LobbyMatchList_t *pCallback, bool bIOFailure ) { m_ListLobbies.clear(); m_bRequestingLobbies = false; + if ( bIOFailure ) + { + // we had a Steam I/O failure - we probably timed out talking to the Steam back-end servers + // doesn't matter in this case, we can just act if no lobbies were received + } + // lobbies are returned in order of closeness to the user, so add them to the list in that order for ( uint32 iLobby = 0; iLobby < pCallback->m_nLobbiesMatching; iLobby++ ) { diff --git a/SteamworksExample/Lobby.h b/SteamworksExample/Lobby.h index f11ccb4..7aae33e 100644 --- a/SteamworksExample/Lobby.h +++ b/SteamworksExample/Lobby.h @@ -84,7 +84,8 @@ private: // Menu object CLobbyBrowserMenu *m_pMenu; - STEAM_CALLBACK( CLobbyBrowser, OnLobbyMatchListCallback, LobbyMatchList_t, m_CallbackLobbyMatchList ); + CCallResult m_SteamCallResultLobbyMatchList; + void OnLobbyMatchListCallback( LobbyMatchList_t *pLobbyMatchList, bool bIOFailure ); STEAM_CALLBACK( CLobbyBrowser, OnLobbyDataUpdatedCallback, LobbyDataUpdate_t, m_CallbackLobbyDataUpdated ); std::list< Lobby_t > m_ListLobbies; diff --git a/SteamworksExample/Main.cpp b/SteamworksExample/Main.cpp index afc66cb..ef0ec1a 100644 --- a/SteamworksExample/Main.cpp +++ b/SteamworksExample/Main.cpp @@ -76,7 +76,8 @@ int APIENTRY RealMain(HINSTANCE hInstance, // Generally you should use the default and not call this as users will be most comfortable with // the default position. The API is provided in case the bottom right creates a serious conflict // with important UI in your game. - SteamUtils()->SetOverlayNotificationPosition( k_EPositionTopRight ); +// disabled temporarily until calling issue is resolved +// SteamUtils()->SetOverlayNotificationPosition( k_EPositionTopRight ); // Look for the +connect ipaddress:port parameter in the command line, // Steam will pass this when a user has used the Steam Server browser to find diff --git a/SteamworksExample/MainMenu.cpp b/SteamworksExample/MainMenu.cpp index d713ac0..1f46305 100644 --- a/SteamworksExample/MainMenu.cpp +++ b/SteamworksExample/MainMenu.cpp @@ -22,5 +22,6 @@ CMainMenu::CMainMenu( CGameEngine *pGameEngine ) : CBaseMenu( AddMenuItem( MenuItem_t( "Find Lobby", k_EClientFindLobby ) ); AddMenuItem( MenuItem_t( "Instructions", k_EClientGameInstructions ) ); AddMenuItem( MenuItem_t( "Stats and Achievements", k_EClientStatsAchievements ) ); + AddMenuItem( MenuItem_t( "Remote Storage", k_EClientRemoteStorage ) ); AddMenuItem( MenuItem_t( "Exit Game", k_EClientGameExiting ) ); } \ No newline at end of file diff --git a/SteamworksExample/RemoteStorage.cpp b/SteamworksExample/RemoteStorage.cpp new file mode 100644 index 0000000..c2ea82e --- /dev/null +++ b/SteamworksExample/RemoteStorage.cpp @@ -0,0 +1,253 @@ +//========= Copyright © 1996-2008, Valve LLC, All rights reserved. ============ +// +// Purpose: Class for tracking stats and achievements +// +// $NoKeywords: $ +//============================================================================= + +#include "stdafx.h" +#include "RemoteStorage.h" +#include "BaseMenu.h" + +#define CLOUDDISP_FONT_HEIGHT 20 +#define CLOUDDISP_COLUMN_WIDTH 600 +#define CLOUDDISP_TEXT_HEIGHT 20 +#define CLOUDDISP_VERT_SPACING 4 + +#define MESSAGE_FILE_NAME "message.dat" + + +//----------------------------------------------------------------------------- +// NOTE +// +// The Steam program is normally responsible for synchronizing an App's files +// to the Steam Cloud before launch and after the program exits. +// +// This means that, if you build this example app and run it directly, +// the Remote Storage page may appear to work (it will save the file changes +// to disk, locally), however nothing will actually get pulled down from +// or sent up to the Steam Cloud. +// +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CRemoteStorage::CRemoteStorage( CGameEngine *pGameEngine ) +: m_pGameEngine( pGameEngine ) +{ + m_rgchGreeting[0] = 0; + strncpy( m_rgchGreeting, "", sizeof( m_rgchGreeting ) ); + m_rgchGreetingNext[0] = 0; + + m_pSteamRemoteStorage = SteamRemoteStorage(); + + m_hDisplayFont = pGameEngine->HCreateFont( CLOUDDISP_FONT_HEIGHT, FW_MEDIUM, false, "Arial" ); + if ( !m_hDisplayFont ) + OutputDebugString( "RemoteStorage font was not created properly, text won't draw\n" ); + + GetFileStats(); + + if ( m_pSteamRemoteStorage->FileExists( MESSAGE_FILE_NAME ) ) + { + int32 cubFile = m_pSteamRemoteStorage->GetFileSize( MESSAGE_FILE_NAME ); + + if ( cubFile >= sizeof( m_rgchGreeting ) ) + { + // ?? too big, nuke it + char c = 0; + OutputDebugString( "RemoteStorage: File was larger than expected. . .\n" ); + m_pSteamRemoteStorage->FileWrite( MESSAGE_FILE_NAME, &c, 1 ); + } + else + { + int32 cubRead = m_pSteamRemoteStorage->FileRead( MESSAGE_FILE_NAME, m_rgchGreeting, sizeof( m_rgchGreeting ) - 1 ); + m_rgchGreeting[cubRead] = 0; // null-terminate + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Update stats on our files in the Cloud +//----------------------------------------------------------------------------- +void CRemoteStorage::GetFileStats() +{ + m_nBytesQuota = 0; + m_nAvailableBytes = 0; + m_nNumFilesInCloud = m_pSteamRemoteStorage->GetFileCount(); + m_pSteamRemoteStorage->GetQuota( &m_nBytesQuota, &m_nAvailableBytes ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Render the Remote Storage page +//----------------------------------------------------------------------------- +void CRemoteStorage::Render() +{ + static uint64 m_ulLastEnterKeyTick = 0; + uint64 ulCurrentTickCount = m_pGameEngine->GetGameTickCount(); + + m_bFinished = false; + + // Update key press information + + int nGreetingNextLength = (int) strlen( m_rgchGreetingNext ); + + DWORD dwVKDown = 0; + + bool bQuotaExceeded = nGreetingNextLength > m_nBytesQuota; + + while ( m_pGameEngine->BGetFirstKeyDown( &dwVKDown ) ) + { + // Clear this key + m_pGameEngine->RecordKeyUp( dwVKDown ); + + + if ( VK_ESCAPE == dwVKDown ) + { + // cancel + m_rgchGreetingNext[0] = 0; + + m_bFinished = true; + return; + } + else if ( VK_RETURN == dwVKDown ) + { + if ( !bQuotaExceeded ) + { + uint64 ulCurrentTickCount = m_pGameEngine->GetGameTickCount(); + if ( ulCurrentTickCount - 150 > g_ulLastReturnKeyTick ) + { + // global from BaseMenu.h! + g_ulLastReturnKeyTick = ulCurrentTickCount; + + // Do it + { + m_bFinished = true; + + strncpy( m_rgchGreeting, m_rgchGreetingNext, sizeof( m_rgchGreeting ) ); + m_rgchGreetingNext[0] = 0; + + // Note: not writing the NULL termination, so won't read it back later either. + bool bRet = m_pSteamRemoteStorage->FileWrite( MESSAGE_FILE_NAME, m_rgchGreeting, (int) strlen( m_rgchGreeting ) ); + + // Update our stats on stuff + GetFileStats(); + + if ( !bRet ) + { + OutputDebugString( "RemoteStorage: Failed to write file!\n" ); + } + + return; + } + } + } + } + else if ( VK_BACK == dwVKDown ) + { + if ( nGreetingNextLength ) + { + m_rgchGreetingNext[--nGreetingNextLength] = 0; + } + } + else if ( ( dwVKDown >= 0x30 && dwVKDown <= 0x39 ) + || ( dwVKDown >= 0x41 && dwVKDown <= 0x5A ) + || dwVKDown == VK_SPACE ) + { + // Add the key pressed + if ( nGreetingNextLength + 1 < sizeof( m_rgchGreetingNext ) ) + { + m_rgchGreetingNext[nGreetingNextLength++] = (char) dwVKDown; + m_rgchGreetingNext[nGreetingNextLength] = 0; + } + } + } + + const int32 width = m_pGameEngine->GetViewportWidth(); + const int32 height = m_pGameEngine->GetViewportHeight(); + + const int32 pxColumn1Left = width / 2 - CLOUDDISP_COLUMN_WIDTH / 2; + + RECT rect; + { + int32 pxVertOffset = 8 * ( CLOUDDISP_TEXT_HEIGHT ); + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_VERT_SPACING; + + char rgchBuffer[256]; + _snprintf( rgchBuffer, sizeof( rgchBuffer), "Num Files In Cloud: %d", m_nNumFilesInCloud ); + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, rgchBuffer ); + + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_TEXT_HEIGHT + CLOUDDISP_VERT_SPACING; + + _snprintf( rgchBuffer, sizeof( rgchBuffer), "Quota: %d bytes, %d bytes remaining", m_nBytesQuota, m_nAvailableBytes ); + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, rgchBuffer ); + + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_VERT_SPACING; + + _snprintf( rgchBuffer, sizeof( rgchBuffer), "Current Message:" ); + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, rgchBuffer ); + + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_TEXT_HEIGHT + CLOUDDISP_VERT_SPACING; + + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, m_rgchGreeting ); + + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_VERT_SPACING; + + _snprintf( rgchBuffer, sizeof( rgchBuffer), "Type in a new message below:" ); + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, rgchBuffer ); + + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_TEXT_HEIGHT + CLOUDDISP_VERT_SPACING; + + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, m_rgchGreetingNext ); + + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_TEXT_HEIGHT + CLOUDDISP_VERT_SPACING; + + _snprintf( rgchBuffer, sizeof( rgchBuffer), "Hit to save, to cancel" ); + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, rgchBuffer ); + + if ( bQuotaExceeded ) + { + rect.top = pxVertOffset; + rect.bottom = rect.top + CLOUDDISP_TEXT_HEIGHT; + rect.left = pxColumn1Left; + rect.right = rect.left + CLOUDDISP_COLUMN_WIDTH; + pxVertOffset = rect.bottom + CLOUDDISP_TEXT_HEIGHT + CLOUDDISP_VERT_SPACING; + + _snprintf( rgchBuffer, sizeof( rgchBuffer), "!! QUOTA EXCEEDED !!" ); + m_pGameEngine->BDrawString( m_hDisplayFont, rect, D3DCOLOR_ARGB( 255, 25, 200, 25 ), DT_CENTER|DT_VCENTER, rgchBuffer ); + } + + } +} + + diff --git a/SteamworksExample/RemoteStorage.h b/SteamworksExample/RemoteStorage.h new file mode 100644 index 0000000..1aafe95 --- /dev/null +++ b/SteamworksExample/RemoteStorage.h @@ -0,0 +1,59 @@ +//========= Copyright © 1996-2008, Valve LLC, All rights reserved. ============ +// +// Purpose: Class for tracking stats and achievements +// +// $NoKeywords: $ +//============================================================================= + +#ifndef REMOTE_STORAGE_H +#define REMOTE_STORAGE_H + +#include "SpaceWar.h" +#include "GameEngine.h" +#include "steam/isteamremotestorage.h" + +class ISteamUser; +class CSpaceWarClient; + +class CRemoteStorage +{ +public: + + // Constructor + CRemoteStorage( CGameEngine *pGameEngine ); + + // Display the remote storage screen + void Render(); + + // Done showing this page? + bool BFinished() { return m_bFinished; } + + +private: + + void GetFileStats(); + + // Engine + CGameEngine *m_pGameEngine; + + // Display font + HGAMEFONT m_hDisplayFont; + + // Steam User interface + ISteamUser *m_pSteamUser; + + // Steam RemoteStorage interface + ISteamRemoteStorage *m_pSteamRemoteStorage; + + // Greeting message + char m_rgchGreeting[40]; + char m_rgchGreetingNext[40]; + + bool m_bFinished; + + int32 m_nNumFilesInCloud; + int32 m_nBytesQuota; + int32 m_nAvailableBytes; +}; + +#endif diff --git a/SteamworksExample/SpaceWar.h b/SteamworksExample/SpaceWar.h index 89eda00..1d2a7cb 100644 --- a/SteamworksExample/SpaceWar.h +++ b/SteamworksExample/SpaceWar.h @@ -108,6 +108,7 @@ enum EClientGameState k_EClientFindLobby, k_EClientJoiningLobby, k_EClientFindLANServers, + k_EClientRemoteStorage, }; diff --git a/SteamworksExample/SpaceWarClient.cpp b/SteamworksExample/SpaceWarClient.cpp index f1b530d..1b53f20 100644 --- a/SteamworksExample/SpaceWarClient.cpp +++ b/SteamworksExample/SpaceWarClient.cpp @@ -23,9 +23,8 @@ CSpaceWarClient* SpaceWarClient() { return g_pSpaceWarClient; } //----------------------------------------------------------------------------- CSpaceWarClient::CSpaceWarClient( CGameEngine *pGameEngine, CSteamID steamIDUser ) : m_SocketStatusCallback( this, &CSpaceWarClient::OnSocketStatusCallback ), - m_LobbyCreatedCallback( this, &CSpaceWarClient::OnLobbyCreated ), - m_LobbyEnteredCallback( this, &CSpaceWarClient::OnLobbyEntered ), - m_LobbyGameCreated( this, &CSpaceWarClient::OnLobbyGameCreated ) + m_LobbyGameCreated( this, &CSpaceWarClient::OnLobbyGameCreated ), + m_IPCFailureCallback( this, &CSpaceWarClient::OnIPCFailure ) { g_pSpaceWarClient = this; m_pGameEngine = pGameEngine; @@ -38,7 +37,6 @@ CSpaceWarClient::CSpaceWarClient( CGameEngine *pGameEngine, CSteamID steamIDUser m_SteamIDLocalUser = steamIDUser; m_eConnectedStatus = k_EClientNotConnected; m_hSocketClient = NULL; - m_bCreatingLobby = false; m_bTransitionedGameState = false; m_rgchErrorText[0] = 0; m_unServerIP = 0; @@ -80,6 +78,9 @@ CSpaceWarClient::CSpaceWarClient( CGameEngine *pGameEngine, CSteamID steamIDUser // Init stats m_pStatsAndAchievements = new CStatsAndAchievements( pGameEngine ); + + // Remote Storage page + m_pRemoteStorage = new CRemoteStorage( pGameEngine ); } @@ -387,7 +388,7 @@ void CSpaceWarClient::InitiateServerConnection( CSteamID steamIDGameServer ) m_ulLastNetworkDataReceivedTime = m_ulLastConnectionAttemptRetryTime = m_pGameEngine->GetGameTickCount(); // Create UDP socket for the client to talk to the server over - m_hSocketClient = SteamNetworking()->CreateP2PConnectionSocket( steamIDGameServer, 0, 10 ); + m_hSocketClient = SteamNetworking()->CreateP2PConnectionSocket( steamIDGameServer, 0, 10, true ); if ( !m_hSocketClient ) { OutputDebugString( "Failed creating socket for CSpaceWarClient -- won't be able to talk to server\n" ); @@ -561,7 +562,7 @@ void CSpaceWarClient::OnReceiveServerExiting() //----------------------------------------------------------------------------- // Purpose: Finishes up entering a lobby of our own creation //----------------------------------------------------------------------------- -void CSpaceWarClient::OnLobbyCreated( LobbyCreated_t *pCallback ) +void CSpaceWarClient::OnLobbyCreated( LobbyCreated_t *pCallback, bool bIOFailure ) { if ( m_eGameState != k_EClientCreatingLobby ) return; @@ -570,9 +571,16 @@ void CSpaceWarClient::OnLobbyCreated( LobbyCreated_t *pCallback ) if ( pCallback->m_eResult == k_EResultOK ) { // success - // now we just need to wait to join the job we just created - SetGameState( k_EClientJoiningLobby ); m_steamIDLobby = pCallback->m_ulSteamIDLobby; + m_pLobby->SetLobbySteamID( m_steamIDLobby ); + + // set the name of the lobby if it's ours + char rgchLobbyName[256]; + _snprintf( rgchLobbyName, sizeof( rgchLobbyName ), "%s's lobby", SteamFriends()->GetPersonaName() ); + SteamMatchmaking()->SetLobbyData( m_steamIDLobby, "name", rgchLobbyName ); + + // mark that we're in the lobby + SetGameState( k_EClientInLobby ); } else { @@ -586,7 +594,7 @@ void CSpaceWarClient::OnLobbyCreated( LobbyCreated_t *pCallback ) //----------------------------------------------------------------------------- // Purpose: Finishes up entering a lobby //----------------------------------------------------------------------------- -void CSpaceWarClient::OnLobbyEntered( LobbyEnter_t *pCallback ) +void CSpaceWarClient::OnLobbyEntered( LobbyEnter_t *pCallback, bool bIOFailure ) { if ( m_eGameState != k_EClientJoiningLobby ) return; @@ -597,14 +605,6 @@ void CSpaceWarClient::OnLobbyEntered( LobbyEnter_t *pCallback ) m_steamIDLobby = pCallback->m_ulSteamIDLobby; m_pLobby->SetLobbySteamID( m_steamIDLobby ); - // set the name of the lobby if it's ours - if ( m_bCreatingLobby ) - { - m_bCreatingLobby = false; - char rgchLobbyName[256]; - _snprintf( rgchLobbyName, sizeof( rgchLobbyName ), "%s's lobby", SteamFriends()->GetPersonaName() ); - SteamMatchmaking()->SetLobbyData( m_steamIDLobby, "name", rgchLobbyName ); - } } @@ -685,14 +685,12 @@ void CSpaceWarClient::OnGameStateChanged( EClientGameState eGameStateNew ) else if ( m_eGameState == k_EClientCreatingLobby ) { // start creating the lobby - if ( !m_bCreatingLobby ) + if ( !m_SteamCallResultLobbyCreated.IsActive() ) { - m_bCreatingLobby = true; - // ask steam to create a lobby - SteamMatchmaking()->CreateLobby( false /* public lobby, anyone can find it */ ); - - // result is returned through LobbyCreated_t callback, wait for that + SteamAPICall_t hSteamAPICall = SteamMatchmaking()->CreateLobby( k_ELobbyTypePublic /* public lobby, anyone can find it */ ); + // set the function to call when this completes + m_SteamCallResultLobbyCreated.Set( hSteamAPICall, this, &CSpaceWarClient::OnLobbyCreated ); } } else if ( m_eGameState == k_EClientFindLobby ) @@ -715,6 +713,23 @@ void CSpaceWarClient::OnGameStateChanged( EClientGameState eGameStateNew ) } } +//----------------------------------------------------------------------------- +// Purpose: Handles notification of a steam ipc failure +// we may get multiple callbacks, one for each IPC operation we attempted +// since the actual failure, so protect ourselves from alerting more than once. +//----------------------------------------------------------------------------- +void CSpaceWarClient::OnIPCFailure( IPCFailure_t *failure ) +{ + static bool bExiting = false; + if ( ! bExiting ) + { + OutputDebugString( "Steam IPC Failure, shutting down\n" ); + ::MessageBoxA( NULL, "Connection to Steam Lost, Exiting", "Steam Connection Error", MB_OK ); + m_pGameEngine->Shutdown(); + bExiting = true; + } +} + //----------------------------------------------------------------------------- // Purpose: Main frame function, updates the state of the world and performs rendering @@ -866,6 +881,14 @@ void CSpaceWarClient::RunFrame() if ( bEscapePressed ) SetGameState( k_EClientGameMenu ); break; + case k_EClientRemoteStorage: + m_pStarField->Render(); + m_pRemoteStorage->Render(); + + if ( m_pRemoteStorage->BFinished() ) + SetGameState( k_EClientGameMenu ); + + break; case k_EClientGameStartServer: m_pStarField->Render(); if ( !m_pServer ) diff --git a/SteamworksExample/SpaceWarClient.h b/SteamworksExample/SpaceWarClient.h index 04383d8..a3a804c 100644 --- a/SteamworksExample/SpaceWarClient.h +++ b/SteamworksExample/SpaceWarClient.h @@ -17,6 +17,7 @@ #include "Ship.h" #include "steam\steam_api.h" #include "StatsAndAchievements.h" +#include "RemoteStorage.h" #include "BaseMenu.h" // Forward class declaration @@ -110,9 +111,9 @@ public: // start joining the lobby if ( selection.m_eStateToTransitionTo == k_EClientJoiningLobby ) { - SteamMatchmaking()->JoinLobby( selection.m_steamIDLobby ); - - // the callback LobbyEnter_t will be received when we've joined + SteamAPICall_t hSteamAPICall = SteamMatchmaking()->JoinLobby( selection.m_steamIDLobby ); + // set the function to call when this API completes + m_SteamCallResultLobbyEntered.Set( hSteamAPICall, this, &CSpaceWarClient::OnLobbyEntered ); } SetGameState( selection.m_eStateToTransitionTo ); @@ -257,14 +258,18 @@ private: CServerBrowser *m_pServerBrowser; + CRemoteStorage *m_pRemoteStorage; + // lobby handling - bool m_bCreatingLobby; // the name of the lobby we're connected to CSteamID m_steamIDLobby; // callback for when we're creating a new lobby - STEAM_CALLBACK( CSpaceWarClient, OnLobbyCreated, LobbyCreated_t, m_LobbyCreatedCallback ); + void OnLobbyCreated( LobbyCreated_t *pCallback, bool bIOFailure ); + CCallResult m_SteamCallResultLobbyCreated; // callback for when we've joined a lobby - STEAM_CALLBACK( CSpaceWarClient, OnLobbyEntered, LobbyEnter_t, m_LobbyEnteredCallback ); + void OnLobbyEntered( LobbyEnter_t *pCallback, bool bIOFailure ); + CCallResult m_SteamCallResultLobbyEntered; + // callback for when the lobby game server has started STEAM_CALLBACK( CSpaceWarClient, OnLobbyGameCreated, LobbyGameCreated_t, m_LobbyGameCreated ); @@ -276,6 +281,9 @@ private: // connection handler STEAM_CALLBACK( CSpaceWarClient, OnSocketStatusCallback, SocketStatusCallback_t, m_SocketStatusCallback ); + + // ipc failure handler + STEAM_CALLBACK( CSpaceWarClient, OnIPCFailure, IPCFailure_t, m_IPCFailureCallback ); }; extern CSpaceWarClient *g_pSpaceWarClient; diff --git a/SteamworksExample/SpaceWarServer.cpp b/SteamworksExample/SpaceWarServer.cpp index 4d70b50..26c734a 100644 --- a/SteamworksExample/SpaceWarServer.cpp +++ b/SteamworksExample/SpaceWarServer.cpp @@ -102,7 +102,7 @@ CSpaceWarServer::CSpaceWarServer( CGameEngine *pGameEngine ) : ResetPlayerShips(); // Start listening for client connections, as we now have enough info to really handle them - m_hSocketServer = SteamGameServerNetworking()->CreateListenSocket( 0, 0, SPACEWAR_SERVER_PORT ); + m_hSocketServer = SteamGameServerNetworking()->CreateListenSocket( 0, 0, SPACEWAR_SERVER_PORT, true ); if ( !m_hSocketServer ) { OutputDebugString( "Failed creating socket for CSpaceWarServer -- no one will be able to connect\n" ); diff --git a/SteamworksExample/SteamworksExample.vcproj b/SteamworksExample/SteamworksExample.vcproj index 4fc09db..58ae113 100644 --- a/SteamworksExample/SteamworksExample.vcproj +++ b/SteamworksExample/SteamworksExample.vcproj @@ -28,8 +28,8 @@ /> + + diff --git a/public/steam/isteamclient.h b/public/steam/isteamclient.h index 6466a15..4573d9b 100644 --- a/public/steam/isteamclient.h +++ b/public/steam/isteamclient.h @@ -36,6 +36,7 @@ class ISteamMatchmakingServers; class ISteamUserStats; class ISteamApps; class ISteamNetworking; +class ISteamRemoteStorage; //----------------------------------------------------------------------------- // Purpose: Interface to creating a new steam instance, or to @@ -120,6 +121,10 @@ public: // 'const char *' is the text of the message // callbacks will occur directly after the API function is called that generated the warning or message virtual void SetWarningMessageHook( SteamAPIWarningMessageHook_t pFunction ) = 0; + + // remote storage + virtual ISteamRemoteStorage *GetISteamRemoteStorage( HSteamUser hSteamuser, HSteamPipe hSteamPipe, const char *pchVersion ) = 0; + }; #define STEAMCLIENT_INTERFACE_VERSION "SteamClient007" diff --git a/public/steam/isteamfriends.h b/public/steam/isteamfriends.h index f109be3..3ebbb1f 100644 --- a/public/steam/isteamfriends.h +++ b/public/steam/isteamfriends.h @@ -73,6 +73,17 @@ enum EAvatarSize }; +// friend game played information +struct FriendGameInfo_t +{ + CGameID m_gameID; + uint32 m_unGameIP; + uint16 m_usGamePort; + uint16 m_usQueryPort; + CSteamID m_steamIDLobby; +}; + + // maximum number of characters in a users name enum { k_cchPersonaNameMax = 128 }; @@ -125,8 +136,8 @@ public: // gets the avatar of the current user, which is a handle to be used in IClientUtils::GetImageRGBA(), or 0 if none set virtual int GetFriendAvatar( CSteamID steamIDFriend, int eAvatarSize ) = 0; - // returns true if the friend is actually in a game - virtual bool GetFriendGamePlayed( CSteamID steamIDFriend, uint64 *pulGameID, uint32 *punGameIP, uint16 *pusGamePort, uint16 *pusQueryPort ) = 0; + // returns true if the friend is actually in a game, and fills in pFriendGameInfo with an extra details + virtual bool GetFriendGamePlayed( CSteamID steamIDFriend, FriendGameInfo_t *pFriendGameInfo ) = 0; // accesses old friends names - returns an empty string when their are no more items in the history virtual const char *GetFriendPersonaNameHistory( CSteamID steamIDFriend, int iPersonaName ) = 0; @@ -151,11 +162,22 @@ public: // User is in a game pressing the talk button (will suppress the microphone for all voice comms from the Steam friends UI) virtual void SetInGameVoiceSpeaking( CSteamID steamIDUser, bool bSpeaking ) = 0; - // activates the game overlay, with an optional dialog to open ("Friends", "Community", "Players", "Settings") + // activates the game overlay, with an optional dialog to open + // valid options are "Friends", "Community", "Players", "Settings", "LobbyInvite", "OfficialGameGroup" virtual void ActivateGameOverlay( const char *pchDialog ) = 0; + + // activates game overlay to a specific place + // valid options are + // "steamid" - opens the overlay web browser to the specified user or groups profile + // "chat" - opens a chat window to the specified user, or joins the group chat + virtual void ActivateGameOverlayToUser( const char *pchDialog, CSteamID steamID ) = 0; + + // activates game overlay web browser directly to the specified URL + // full address with protocol type is required, e.g. http://www.steamgames.com/ + virtual void ActivateGameOverlayToWebPage( const char *pchURL ) = 0; }; -#define STEAMFRIENDS_INTERFACE_VERSION "SteamFriends004" +#define STEAMFRIENDS_INTERFACE_VERSION "SteamFriends005" //----------------------------------------------------------------------------- // Purpose: called when a friends' status changes @@ -209,4 +231,17 @@ struct GameServerChangeRequested_t char m_rgchPassword[64]; // server password, if any }; + +//----------------------------------------------------------------------------- +// Purpose: called when the user tries to join a lobby from their friends list +// game client should attempt to connect to specified lobby when this is received +//----------------------------------------------------------------------------- +struct GameLobbyJoinRequested_t +{ + enum { k_iCallback = k_iSteamFriendsCallbacks + 33 }; + CSteamID m_steamIDLobby; + CSteamID m_steamIDFriend; // the friend they did the join via (will be invalid if not directly via a friend) +}; + + #endif // ISTEAMFRIENDS_H diff --git a/public/steam/isteamgameserver.h b/public/steam/isteamgameserver.h index 65e5046..d3e1db5 100644 --- a/public/steam/isteamgameserver.h +++ b/public/steam/isteamgameserver.h @@ -89,9 +89,21 @@ public: // Ask if a user has a specific achievement for this game, will get a callback on reply virtual bool BGetUserAchievementStatus( CSteamID steamID, const char *pchAchievementName ) = 0; + + // Ask for the gameplay stats for the server. Results returned in a callback + virtual void GetGameplayStats( ) = 0; + + // Ask if a user in in the specified group, results returns async by GSUserGroupStatus_t + // returns false if we're not connected to the steam servers and thus cannot ask + virtual bool RequestUserGroupStatus( CSteamID steamIDUser, CSteamID steamIDGroup ) = 0; + + // Returns the public IP of the server according to Steam, useful when the server is + // behind NAT and you want to advertise its IP in a lobby for other clients to directly + // connect to + virtual uint32 GetPublicIP() = 0; }; -#define STEAMGAMESERVER_INTERFACE_VERSION "SteamGameServer005" +#define STEAMGAMESERVER_INTERFACE_VERSION "SteamGameServer008" // game server flags const uint32 k_unServerFlagNone = 0x00; @@ -156,6 +168,25 @@ struct GSPolicyResponse_t uint8 m_bSecure; }; +// GS gameplay stats info +struct GSGameplayStats_t +{ + enum { k_iCallback = k_iSteamGameServerCallbacks + 7 }; + EResult m_eResult; // Result of the call + int32 m_nRank; // Overall rank of the server (0-based) + uint32 m_unTotalConnects; // Total number of clients who have ever connected to the server + uint32 m_unTotalMinutesPlayed; // Total number of minutes ever played on the server +}; + +// send as a reply to RequestUserGroupStatus() +struct GSClientGroupStatus_t +{ + enum { k_iCallback = k_iSteamGameServerCallbacks + 8 }; + CSteamID m_SteamIDUser; + CSteamID m_SteamIDGroup; + bool m_bMember; + bool m_bOfficer; +}; #endif // ISTEAMGAMESERVER_H diff --git a/public/steam/isteammatchmaking.h b/public/steam/isteammatchmaking.h index 95b58f0..2ec761b 100644 --- a/public/steam/isteammatchmaking.h +++ b/public/steam/isteammatchmaking.h @@ -16,6 +16,23 @@ #include "isteamclient.h" #include "isteamfriends.h" +//----------------------------------------------------------------------------- +// Purpose: Functions for match making services for clients to get to favorites +//----------------------------------------------------------------------------- +enum ELobbyType +{ + k_ELobbyTypeFriendsOnly = 1, // shows for friends or invitees, but not in lobby list + k_ELobbyTypePublic = 2, // visible for friends and in lobby list +}; + +enum ELobbyComparison +{ + k_ELobbyComparisonEqualToOrLessThan = -2, + k_ELobbyComparisonLessThan = -1, + k_ELobbyComparisonEqual = 0, + k_ELobbyComparisonGreaterThan = 1, + k_ELobbyComparisonEqualToOrGreaterThan = 2, +}; //----------------------------------------------------------------------------- // Purpose: Functions for match making services for clients to get to favorites @@ -48,19 +65,37 @@ public: // Get a list of relevant lobbies // this is an asynchronous request - // results will be returned by LobbyMatchList_t callback, with the number of servers requested - // if the user is not currently connected to Steam (i.e. SteamUser()->BLoggedOn() returns false) then - // a LobbyMatchList_t callback will be posted immediately with no servers - virtual void RequestLobbyList() = 0; + // results will be returned by LobbyMatchList_t callback & call result, with the number of lobbies found + // this will never return lobbies that are full + // to add more filter, the filter calls below need to be call before each and every RequestLobbyList() call + // use the CCallResult<> object in steam_api.h to match the SteamAPICall_t call result to a function in an object, e.g. + /* + class CMyLobbyListManager + { + CCallResult m_CallResultLobbyMatchList; + void FindLobbies() + { + // SteamMatchmaking()->AddRequestLobbyListFilter*() functions would be called here, before RequestLobbyList() + SteamAPICall_t hSteamAPICall = SteamMatchmaking()->RequestLobbyList(); + m_CallResultLobbyMatchList.Set( hSteamAPICall, this, &CMyLobbyListManager::OnLobbyMatchList ); + } + void OnLobbyMatchList( LobbyMatchList_t *pLobbyMatchList, bool bIOFailure ) + { + // lobby list has be retrieved from Steam back-end, use results + } + } + */ + // + virtual SteamAPICall_t RequestLobbyList() = 0; // filters for lobbies // this needs to be called before RequestLobbyList() to take effect // these are cleared on each call to RequestLobbyList() virtual void AddRequestLobbyListFilter( const char *pchKeyToMatch, const char *pchValueToMatch ) = 0; - // numerical comparison - 0 is equal, -1 is the lobby value is less than nValueToMatch, 1 is the lobby value is greater than nValueToMatch - virtual void AddRequestLobbyListNumericalFilter( const char *pchKeyToMatch, int nValueToMatch, int nComparisonType /* 0 is equal, -1 is less than, 1 is greater than */ ) = 0; - // sets RequestLobbyList() to only returns lobbies which aren't yet full - needs SetLobbyMemberLimit() called on the lobby to set an initial limit - virtual void AddRequestLobbyListSlotsAvailableFilter() = 0; + // numerical comparison + virtual void AddRequestLobbyListNumericalFilter( const char *pchKeyToMatch, int nValueToMatch, int nComparisonType ) = 0; + // returns results closest to the specified value. Multiple near filters can be added, with early filters taking precedence + virtual void AddRequestLobbyListNearValueFilter( const char *pchKeyToMatch, int nValueToBeCloseTo ) = 0; // returns the CSteamID of a lobby, as retrieved by a RequestLobbyList call // should only be called after a LobbyMatchList_t callback is received @@ -69,19 +104,18 @@ public: virtual CSteamID GetLobbyByIndex( int iLobby ) = 0; // Create a lobby on the Steam servers. - // If bPrivate is true, then the lobby will not be returned by any RequestLobbyList() call; the CSteamID + // If private, then the lobby will not be returned by any RequestLobbyList() call; the CSteamID // of the lobby will need to be communicated via game channels or via InviteUserToLobby() // this is an asynchronous request - // results will be returned by LobbyCreated_t callback when the lobby has been created; - // local user will the join the lobby, resulting in an additional LobbyEnter_t callback being sent - // operations on the chat room can only proceed once the LobbyEnter_t has been received - virtual void CreateLobby( bool bPrivate ) = 0; + // results will be returned by LobbyCreated_t callback and call result; lobby is joined & ready to use at this pointer + // a LobbyEnter_t callback will also be received (since the local user is joining their own lobby) + virtual SteamAPICall_t CreateLobby( ELobbyType eLobbyType ) = 0; // Joins an existing lobby // this is an asynchronous request - // results will be returned by LobbyEnter_t callback when the lobby has been joined - // users already in the lobby will receive LobbyChatUpdate_t callback after this user has successfully joined - virtual void JoinLobby( CSteamID steamIDLobby ) = 0; + // results will be returned by LobbyEnter_t callback & call result, check m_EChatRoomEnterResponse to see if was successful + // lobby metadata is available to use immediately on this call completing + virtual SteamAPICall_t JoinLobby( CSteamID steamIDLobby ) = 0; // Leave a lobby; this will take effect immediately on the client side // other users in the lobby will be notified by a LobbyChatUpdate_t callback @@ -133,14 +167,16 @@ public: // return value is the number of bytes written into the buffer virtual int GetLobbyChatEntry( CSteamID steamIDLobby, int iChatID, CSteamID *pSteamIDUser, void *pvData, int cubData, EChatEntryType *peChatEntryType ) = 0; - // Fetch metadata for a lobby you're not necessarily in right now + // Refreshes metadata for a lobby you're not necessarily in right now + // you never do this for lobbies you're a member of, only if your // this will send down all the metadata associated with a lobby // this is an asynchronous call // returns false if the local user is not connected to the Steam servers + // restart are returned by a LobbyDataUpdate_t callback virtual bool RequestLobbyData( CSteamID steamIDLobby ) = 0; // sets the game server associated with the lobby - // usually at this point, the users will leave the lobby and join the specified game server + // usually at this point, the users will join the specified game server // either the IP/Port or the steamID of the game server has to be valid, depending on how you want the clients to be able to connect virtual void SetLobbyGameServer( CSteamID steamIDLobby, uint32 unGameServerIP, uint16 unGameServerPort, CSteamID steamIDGameServer ) = 0; // returns the details of a game server set in a lobby - returns false if there is no game server set, or that lobby doesn't exist @@ -151,13 +187,17 @@ public: // returns the current limit on the # of users who can join the lobby; returns 0 if no limit is defined virtual int GetLobbyMemberLimit( CSteamID steamIDLobby ) = 0; - // asks the Steam servers for a list of lobbies that friends are in - // returns results by posting one RequestFriendsLobbiesResponse_t callback per friend/lobby pair - // if no friends are in lobbies, RequestFriendsLobbiesResponse_t will be posted but with 0 results - // filters don't apply to lobbies (currently) - virtual bool RequestFriendsLobbies() = 0; + // updates which type of lobby it is + // only lobbies that are k_ELobbyTypePublic will be returned by RequestLobbyList() calls + virtual bool SetLobbyType( CSteamID steamIDLobby, ELobbyType eLobbyType ) = 0; + + // returns the current lobby owner + // you must be a member of the lobby to access this + // there always one lobby owner - if the current owner leaves, another user will become the owner + // it is possible (bur rare) to join a lobby just as the owner is leaving, thus entering a lobby with self as the owner + virtual CSteamID GetLobbyOwner( CSteamID steamIDLobby ) = 0; }; -#define STEAMMATCHMAKING_INTERFACE_VERSION "SteamMatchMaking004" +#define STEAMMATCHMAKING_INTERFACE_VERSION "SteamMatchMaking006" //----------------------------------------------------------------------------- @@ -358,14 +398,11 @@ const uint32 k_unFavoriteFlagHistory = 0x02; // this game favorite entry is for enum EChatMemberStateChange { // Specific to joining / leaving the chatroom - k_EChatMemberStateChangeEntered = 0x01, // This user has joined or is joining the chat room - k_EChatMemberStateChangeLeft = 0x02, // This user has left or is leaving the chat room - k_EChatMemberStateChangeDisconnected = 0x04, // User disconnected without leaving the chat first - k_EChatMemberStateChangeKicked = 0x08, // User kicked - k_EChatMemberStateChangeBanned = 0x10, // User kicked and banned - - k_EChatMemberInfoVoiceSpeaking = 0x20, // User started talking (using speaker slot) - k_EChatMemberInfoVoiceDoneSpeaking = 0x40, // User relinquished speaker slot + k_EChatMemberStateChangeEntered = 0x0001, // This user has joined or is joining the chat room + k_EChatMemberStateChangeLeft = 0x0002, // This user has left or is leaving the chat room + k_EChatMemberStateChangeDisconnected = 0x0004, // User disconnected without leaving the chat first + k_EChatMemberStateChangeKicked = 0x0008, // User kicked + k_EChatMemberStateChangeBanned = 0x0010, // User kicked and banned }; // returns true of the flags indicate that a user has been removed from the chat @@ -407,7 +444,7 @@ struct LobbyInvite_t //----------------------------------------------------------------------------- -// Purpose: Sent on entering a Lobby +// Purpose: Sent on entering a lobby, or on failing to enter // m_EChatRoomEnterResponse will be set to k_EChatRoomEnterResponseSuccess on success, // or a higher value on failure (see enum EChatRoomEnterResponse) //----------------------------------------------------------------------------- @@ -527,26 +564,20 @@ struct LobbyKicked_t struct LobbyCreated_t { enum { k_iCallback = k_iSteamMatchmakingCallbacks + 13 }; - EResult m_eResult; // Result + + EResult m_eResult; // k_EResultOK - the lobby was successfully created + // k_EResultNoConnection - your Steam client doesn't have a connection to the back-end + // k_EResultTimeout - you the message to the Steam servers, but it didn't respond + // k_EResultFail - the server responded, but with an unknown internal error + // k_EResultAccessDenied - your game isn't set to allow lobbies, or your client does haven't rights to play the game + // k_EResultLimitExceeded - your game client has created too many lobbies + uint64 m_ulSteamIDLobby; // chat room, zero if failed }; -//----------------------------------------------------------------------------- -// Purpose: Response to a RequestFriendsLobbies() call -// One of these callbacks will be received per friend who is in a lobby -// if no friends are in a lobby, then one of these will be called with 0 values -//----------------------------------------------------------------------------- -struct RequestFriendsLobbiesResponse_t -{ - enum { k_iCallback = k_iSteamMatchmakingCallbacks + 14 }; - - uint64 m_ulSteamIDFriend; // friend who is in a lobby; 0 if no friends in lobbies are found - uint64 m_ulSteamIDLobby; // lobby that the friend is in; 0 if no friends in lobbies are found - - int m_cResultIndex; // result #, [1, m_cResultsTotal] if any are found; 0 if no friends in lobbies are found - int m_cResultsTotal; // total number of results; 0 if no friends in lobbies are found -}; +// used by now obsolete RequestFriendsLobbiesResponse_t +// enum { k_iCallback = k_iSteamMatchmakingCallbacks + 14 }; diff --git a/public/steam/isteamnetworking.h b/public/steam/isteamnetworking.h index b6fd29e..50d8208 100644 --- a/public/steam/isteamnetworking.h +++ b/public/steam/isteamnetworking.h @@ -45,6 +45,14 @@ enum ESNetSocketState }; +// describes how the socket is currently connected +enum ESNetSocketConnectionType +{ + k_ESNetSocketConnectionTypeNotConnected = 0, + k_ESNetSocketConnectionTypeUDP = 1, + k_ESNetSocketConnectionTypeUDPRelay = 2, +}; + //----------------------------------------------------------------------------- // Purpose: Functions for making connections and sending data between clients, @@ -61,13 +69,13 @@ public: // pass in 0 if you just want the default local IP // unPort is the port to use // pass in 0 if you don't want users to be able to connect via IP/Port, but expect to be always peer-to-peer connections only - virtual SNetListenSocket_t CreateListenSocket( int nVirtualP2PPort, uint32 nIP, uint16 nPort ) = 0; + virtual SNetListenSocket_t CreateListenSocket( int nVirtualP2PPort, uint32 nIP, uint16 nPort, bool bAllowUseOfPacketRelay ) = 0; // creates a socket and begin connection to a remote destination // can connect via a known steamID (client or game server), or directly to an IP // on success will trigger a SocketStatusCallback_t callback // on failure or timeout will trigger a SocketStatusCallback_t callback with a failure code in m_eSNetSocketState - virtual SNetSocket_t CreateP2PConnectionSocket( CSteamID steamIDTarget, int nVirtualPort, int nTimeoutSec ) = 0; + virtual SNetSocket_t CreateP2PConnectionSocket( CSteamID steamIDTarget, int nVirtualPort, int nTimeoutSec, bool bAllowUseOfPacketRelay ) = 0; virtual SNetSocket_t CreateConnectionSocket( uint32 nIP, uint16 nPort, int nTimeoutSec ) = 0; // disconnects the connection to the socket, if any, and invalidates the handle @@ -115,9 +123,14 @@ public: // returns which local port the listen socket is bound to // *pnIP and *pnPort will be 0 if the socket is set to listen for P2P connections only virtual bool GetListenSocketInfo( SNetListenSocket_t hListenSocket, uint32 *pnIP, uint16 *pnPort ) = 0; - + + // returns true to describe how the socket ended up connecting + virtual ESNetSocketConnectionType GetSocketConnectionType( SNetSocket_t hSocket ) = 0; + + // max packet size, in bytes + virtual int GetMaxPacketSize( SNetSocket_t hSocket ) = 0; }; -#define STEAMNETWORKING_INTERFACE_VERSION "SteamNetworking001" +#define STEAMNETWORKING_INTERFACE_VERSION "SteamNetworking002" // callback notification - status of a socket has changed diff --git a/public/steam/isteamremotestorage.h b/public/steam/isteamremotestorage.h new file mode 100644 index 0000000..813a945 --- /dev/null +++ b/public/steam/isteamremotestorage.h @@ -0,0 +1,45 @@ +//====== Copyright © 1996-2008, Valve Corporation, All rights reserved. ======= +// +// Purpose: public interface to user remote file storage in Steam +// +//============================================================================= + +#ifndef ISTEAMREMOTESTORAGE_H +#define ISTEAMREMOTESTORAGE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "isteamclient.h" + +//----------------------------------------------------------------------------- +// Purpose: Functions for accessing, reading and writing files stored remotely +// and cached locally +//----------------------------------------------------------------------------- +class ISteamRemoteStorage +{ + public: + // NOTE + // + // Filenames are case-insensitive, and will be converted to lowercase automatically. + // So "foo.bar" and "Foo.bar" are the same file, and if you write "Foo.bar" then + // iterate the files, the filename returned will be "foo.bar". + // + + // file operations + virtual bool FileWrite( const char *pchFile, const void *pvData, int32 cubData ) = 0; + virtual int32 GetFileSize( const char *pchFile ) = 0; + virtual int32 FileRead( const char *pchFile, void *pvData, int32 cubDataToRead ) = 0; + virtual bool FileExists( const char *pchFile ) = 0; + + // iteration + virtual int32 GetFileCount() = 0; + virtual const char *GetFileNameAndSize( int iFile, int32 *pnFileSizeInBytes ) = 0; + + // quota management + virtual bool GetQuota( int32 *pnTotalBytes, int32 *puAvailableBytes ) = 0; +}; + +#define STEAMREMOTESTORAGE_INTERFACE_VERSION "STEAMREMOTESTORAGE_INTERFACE_VERSION002" + +#endif // ISTEAMREMOTESTORAGE_H diff --git a/public/steam/isteamuser.h b/public/steam/isteamuser.h index 69d4704..c229421 100644 --- a/public/steam/isteamuser.h +++ b/public/steam/isteamuser.h @@ -158,10 +158,15 @@ struct ClientGameServerDeny_t // When getting this message the client should disconnect from Steam, reset any stored Steam state and reconnect. // This usually occurs in the rare event the Steam client has some kind of fatal error. //----------------------------------------------------------------------------- -struct CallbackPipeFailure_t +struct IPCFailure_t { enum { k_iCallback = k_iSteamUserCallbacks + 17 }; + enum EFailureType + { + k_EFailureFlushedCallbackQueue, + k_EFailurePipeFail, + }; + uint8 m_eFailureType; }; - #endif // ISTEAMUSER_H diff --git a/public/steam/isteamuserstats.h b/public/steam/isteamuserstats.h index 85d6b9a..475bdb1 100644 --- a/public/steam/isteamuserstats.h +++ b/public/steam/isteamuserstats.h @@ -18,9 +18,8 @@ const uint32 k_cchStatNameMax = 128; class ISteamUserStats { public: - - // Ask the server to send down this user's data and achievements for nGameID - virtual bool RequestCurrentStats( ) = 0; + // Ask the server to send down this user's data and achievements for this game + virtual bool RequestCurrentStats() = 0; // Data accessors virtual bool GetStat( const char *pchName, int32 *pData ) = 0; @@ -38,7 +37,7 @@ public: // Store the current data on the server, will get a callback when set // And one callback for every new achievement - virtual bool StoreStats( ) = 0; + virtual bool StoreStats() = 0; // Achievement / GroupAchievement metadata @@ -51,10 +50,22 @@ public: // Calling this w/ N out of N progress will NOT set the achievement, the game must still do that. virtual bool IndicateAchievementProgress( const char *pchName, uint32 nCurProgress, uint32 nMaxProgress ) = 0; + // Friends stats & achievements + + // downloads stats for the user + // returns a UserStatsReceived_t received when completed + // if the other user has no stats, UserStatsReceived_t.m_eResult will be set to k_EResultFail + // these stats won't be auto-updated; you'll need to call RequestUserStats() again to refresh any data + virtual SteamAPICall_t RequestUserStats( CSteamID steamIDUser ) = 0; + + // requests stat information for a user, usable after a successful call to RequestUserStats() + virtual bool GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData ) = 0; + virtual bool GetUserStat( CSteamID steamIDUser, const char *pchName, float *pData ) = 0; + virtual bool GetUserAchievement( CSteamID steamIDUser, const char *pchName, bool *pbAchieved ) = 0; }; +#define STEAMUSERSTATS_INTERFACE_VERSION "STEAMUSERSTATS_INTERFACE_VERSION004" -#define STEAMUSERSTATS_INTERFACE_VERSION "STEAMUSERSTATS_INTERFACE_VERSION003" //----------------------------------------------------------------------------- // Purpose: called when the latests stats and achievements have been received @@ -65,6 +76,7 @@ struct UserStatsReceived_t enum { k_iCallback = k_iSteamUserStatsCallbacks + 1 }; uint64 m_nGameID; // Game these stats are for EResult m_eResult; // Success / error fetching the stats + CSteamID m_steamIDUser; // The user for whom the stats are retrieved for }; @@ -78,6 +90,7 @@ struct UserStatsStored_t EResult m_eResult; // success / error }; + //----------------------------------------------------------------------------- // Purpose: result of a request to store the achievements for a game, or an // "indicate progress" call. If both m_nCurProgress and m_nMaxProgress diff --git a/public/steam/isteamutils.h b/public/steam/isteamutils.h index 9199c43..ea430aa 100644 --- a/public/steam/isteamutils.h +++ b/public/steam/isteamutils.h @@ -12,6 +12,20 @@ #include "isteamclient.h" + +// Steam API call failure results +enum ESteamAPICallFailure +{ + k_ESteamAPICallFailureNone = -1, // no failure + k_ESteamAPICallFailureSteamGone = 0, // the local Steam process has gone away + k_ESteamAPICallFailureNetworkFailure = 1, // the network connection to Steam has been broken, or was already broken + // SteamServersDisconnected_t callback will be sent around the same time + // SteamServersConnected_t will be sent when the client is able to talk to the Steam servers again + k_ESteamAPICallFailureInvalidHandle = 2, // the SteamAPICall_t handle passed in no longer exists + k_ESteamAPICallFailureMismatchedCallback = 3,// GetAPICallResult() was called with the wrong callback type for this API call +}; + + //----------------------------------------------------------------------------- // Purpose: interface to user independent utility functions //----------------------------------------------------------------------------- @@ -52,6 +66,12 @@ public: // Sets the position where the overlay instance for the currently calling game should show notifications. // This position is per-game and if this function is called from outside of a game context it will do nothing. virtual void SetOverlayNotificationPosition( ENotificationPosition eNotificationPosition ) = 0; + + // API asynchronous call results + // can be used directly, but more commonly used via the callback dispatch API (see steam_api.h) + virtual bool IsAPICallCompleted( SteamAPICall_t hSteamAPICall, bool *pbFailed ) = 0; + virtual ESteamAPICallFailure GetAPICallFailureReason( SteamAPICall_t hSteamAPICall ) = 0; + virtual bool GetAPICallResult( SteamAPICall_t hSteamAPICall, void *pCallback, int cubCallback, int iCallbackExpected, bool *pbFailed ) = 0; }; #define STEAMUTILS_INTERFACE_VERSION "SteamUtils002" @@ -78,4 +98,17 @@ struct LowBatteryPower_t uint8 m_nMinutesBatteryLeft; }; + +//----------------------------------------------------------------------------- +// Purpose: called when a SteamAsyncCall_t has completed (or failed) +//----------------------------------------------------------------------------- +struct SteamAPICallCompleted_t +{ + enum { k_iCallback = k_iSteamUtilsCallbacks + 3 }; + SteamAPICall_t m_hAsyncCall; +}; + + + + #endif // ISTEAMUTILS_H diff --git a/public/steam/matchmakingtypes.h b/public/steam/matchmakingtypes.h index 75f1181..23090f0 100644 --- a/public/steam/matchmakingtypes.h +++ b/public/steam/matchmakingtypes.h @@ -13,8 +13,10 @@ #endif #ifdef POSIX +#ifndef _snprintf #define _snprintf snprintf #endif +#endif #include #include diff --git a/public/steam/steam_api.h b/public/steam/steam_api.h index 31e1e75..7fdd94d 100644 --- a/public/steam/steam_api.h +++ b/public/steam/steam_api.h @@ -18,6 +18,7 @@ #include "isteamuserstats.h" #include "isteamapps.h" #include "isteamnetworking.h" +#include "isteamremotestorage.h" // Steam API export macro #if defined( _WIN32 ) && !defined( _X360 ) @@ -83,6 +84,7 @@ S_API ISteamUserStats *SteamUserStats(); S_API ISteamApps *SteamApps(); S_API ISteamNetworking *SteamNetworking(); S_API ISteamMatchmakingServers *SteamMatchmakingServers(); +S_API ISteamRemoteStorage *SteamRemoteStorage(); #endif // VERSION_SAFE_STEAM_API_INTERFACES //----------------------------------------------------------------------------------------------------------------------------------------------------------// @@ -102,6 +104,10 @@ S_API void SteamAPI_RunCallbacks(); // functions used by the utility CCallback objects to receive callbacks S_API void SteamAPI_RegisterCallback( class CCallbackBase *pCallback, int iCallback ); S_API void SteamAPI_UnregisterCallback( class CCallbackBase *pCallback ); +// functions used by the utility CCallResult objects to receive async call results +S_API void SteamAPI_RegisterCallResult( class CCallbackBase *pCallback, SteamAPICall_t hAPICall ); +S_API void SteamAPI_UnregisterCallResult( class CCallbackBase *pCallback, SteamAPICall_t hAPICall ); + //----------------------------------------------------------------------------- // Purpose: base for callbacks, @@ -110,19 +116,94 @@ S_API void SteamAPI_UnregisterCallback( class CCallbackBase *pCallback ); class CCallbackBase { public: - CCallbackBase() { m_nCallbackFlags = 0; } + CCallbackBase() { m_nCallbackFlags = 0; m_iCallback = 0; } // don't add a virtual destructor because we export this binary interface across dll's virtual void Run( void *pvParam ) = 0; + virtual void Run( void *pvParam, bool bIOFailure, SteamAPICall_t hSteamAPICall ) = 0; + int GetICallback() { return m_iCallback; } + virtual int GetCallbackSizeBytes() = 0; protected: enum { k_ECallbackFlagsRegistered = 0x01, k_ECallbackFlagsGameServer = 0x02 }; uint8 m_nCallbackFlags; -private: int m_iCallback; friend class CCallbackMgr; }; +//----------------------------------------------------------------------------- +// Purpose: maps a steam async call result to a class member function +// template params: T = local class, P = parameter struct +//----------------------------------------------------------------------------- +template< class T, class P > +class CCallResult : private CCallbackBase +{ +public: + typedef void (T::*func_t)( P*, bool ); + + CCallResult() + { + m_hAPICall = 0; + m_pObj = NULL; + m_Func = NULL; + m_iCallback = P::k_iCallback; + } + + void Set( SteamAPICall_t hAPICall, T *p, func_t func ) + { + if ( m_hAPICall ) + SteamAPI_UnregisterCallResult( this, m_hAPICall ); + + m_hAPICall = hAPICall; + m_pObj = p; + m_Func = func; + + if ( hAPICall ) + SteamAPI_RegisterCallResult( this, hAPICall ); + } + + bool IsActive() + { + return ( m_hAPICall != 0 ); + } + + void Cancel() + { + m_hAPICall = 0; + } + + ~CCallResult() + { + if ( m_hAPICall ) + SteamAPI_UnregisterCallResult( this, m_hAPICall ); + } + +private: + virtual void Run( void *pvParam ) + { + (m_pObj->*m_Func)( (P *)pvParam, false ); + m_hAPICall = 0; + } + void Run( void *pvParam, bool bIOFailure, SteamAPICall_t hSteamAPICall ) + { + if ( hSteamAPICall == m_hAPICall ) + { + (m_pObj->*m_Func)( (P *)pvParam, bIOFailure ); + m_hAPICall = 0; + } + } + int GetCallbackSizeBytes() + { + return sizeof( P ); + } + + SteamAPICall_t m_hAPICall; + T *m_pObj; + func_t m_Func; +}; + + + //----------------------------------------------------------------------------- // Purpose: maps a steam callback to a class member function // template params: T = local class, P = parameter struct @@ -176,6 +257,14 @@ private: { (m_pObj->*m_Func)( (P *)pvParam ); } + virtual void Run( void *pvParam, bool, SteamAPICall_t ) + { + (m_pObj->*m_Func)( (P *)pvParam ); + } + int GetCallbackSizeBytes() + { + return sizeof( P ); + } T *m_pObj; func_t m_Func; @@ -240,6 +329,7 @@ public: ISteamApps* SteamApps() { return m_pSteamApps; } ISteamMatchmakingServers* SteamMatchmakingServers() { return m_pSteamMatchmakingServers; } ISteamNetworking* SteamNetworking() { return m_pSteamNetworking; } + ISteamRemoteStorage* SteamRemoteStorage() { return m_pSteamRemoteStorage; } private: ISteamUser *m_pSteamUser; @@ -250,6 +340,7 @@ private: ISteamApps *m_pSteamApps; ISteamMatchmakingServers *m_pSteamMatchmakingServers; ISteamNetworking *m_pSteamNetworking; + ISteamRemoteStorage *m_pSteamRemoteStorage; }; inline CSteamAPIContext::CSteamAPIContext() @@ -267,6 +358,7 @@ inline void CSteamAPIContext::Clear() m_pSteamApps = NULL; m_pSteamMatchmakingServers = NULL; m_pSteamNetworking = NULL; + m_pSteamRemoteStorage = NULL; } // This function must be inlined so the module using steam_api.dll gets the version names they want. @@ -310,6 +402,10 @@ inline bool CSteamAPIContext::Init() if ( !m_pSteamNetworking ) return false; + m_pSteamRemoteStorage = SteamClient()->GetISteamRemoteStorage( hSteamUser, hSteamPipe, STEAMREMOTESTORAGE_INTERFACE_VERSION ); + if ( !m_pSteamRemoteStorage ) + return false; + return true; } diff --git a/public/steam/steamclientpublic.h b/public/steam/steamclientpublic.h index 00c843a..4e8e608 100644 --- a/public/steam/steamclientpublic.h +++ b/public/steam/steamclientpublic.h @@ -1,4 +1,4 @@ -//========= Copyright © 1996-2008, Valve LLC, All rights reserved. ============ +//========= Copyright � 1996-2008, Valve LLC, All rights reserved. ============ // // Purpose: // @@ -67,6 +67,7 @@ enum EResult k_EResultBlocked = 40, // a user didn't allow it k_EResultIgnored = 41, // target is ignoring sender k_EResultNoMatch = 42, // nothing matching the request found + k_EResultAccountDisabled = 43, }; // Error codes for use with the voice functions @@ -180,72 +181,22 @@ enum EChatRoomEnterResponse typedef void (*PFNLegacyKeyRegistration)( const char *pchCDKey, const char *pchInstallPath ); typedef bool (*PFNLegacyKeyInstalled)(); +const int k_unSteamAccountIDMask = 0xFFFFFFFF; +const int k_unSteamAccountInstanceMask = 0x000FFFFF; -//----------------------------------------------------------------------------- -// Purpose: Persistent item constants -//----------------------------------------------------------------------------- -typedef int32 HNewItemRequest; // Handle to an item generation request -const int k_cchCreateItemLen = 64; // Maximum string length in item create APIs -const uint64 INVALID_ITEM_ID = (uint64)-1; - - -//----------------------------------------------------------------------------- -// Purpose: Persistent item quality (rarity) -//----------------------------------------------------------------------------- -enum EItemQuality +// Special flags for Chat accounts - they go in the top 8 bits +// of the steam ID's "instance", leaving 12 for the actual instances +enum EChatSteamIDInstanceFlags { - // If k_EItemQuality_Any is used in an item request a random quality will be chosen - k_EItemQuality_Any = -1, - k_EItemQuality_Normal = 0, - k_EItemQuality_Common = 1, - k_EItemQuality_Rare = 2, - k_EItemQuality_Unique = 3, + k_EChatAccountInstanceMask = 0x00000FFF, // top 8 bits are flags - //Must be last - k_EItemQuality_Count = 4, + k_EChatInstanceFlagClan = ( k_unSteamAccountInstanceMask + 1 ) >> 1, // top bit + k_EChatInstanceFlagLobby = ( k_unSteamAccountInstanceMask + 1 ) >> 2, // next one down, etc + + // Max of 8 flags }; -//----------------------------------------------------------------------------- -// Purpose: Result codes for SendNewItemReqeust -//----------------------------------------------------------------------------- -enum EItemRequestResult -{ - k_EItemRequestResultOK = 0, // Request succeeded - k_EItemRequestResultDenied = 1, // Request denied - k_EItemRequestResultServerError = 2, // Request failed due to a temporary server error - k_EItemRequestResultTimeout = 3, // Request timed out - k_EItemRequestResultInvalid = 4, // Request was corrupt - k_EItemRequestResultNoMatch = 5, // No item definition matched the request - k_EItemRequestResultUnknownError = 6, // Request failed with an unknown error -}; - - -//----------------------------------------------------------------------------- -// Purpose: Valid operators for BAddNewItemCriteria -//----------------------------------------------------------------------------- -enum EItemCriteriaOperator -{ - k_EOperator_String_EQ = 0, // Field is string equal to value - k_EOperator_Not = 1, // Logical not - k_EOperator_String_Not_EQ = 1, // Field is not string equal to value - k_EOperator_Float_EQ = 2, // Field as a float is equal to value - k_EOperator_Float_Not_EQ = 3, // Field as a float is not equal to value - k_EOperator_Float_LT = 4, // Field as a float is less than value - k_EOperator_Float_Not_LT = 5, // Field as a float is not less than value - k_EOperator_Float_LTE = 6, // Field as a float is less than or equal value - k_EOperator_Float_Not_LTE = 7, // Field as a float is not less than or equal value - k_EOperator_Float_GT = 8, // Field as a float is greater than value - k_EOperator_Float_Not_GT = 9, // Field as a float is not greater than value - k_EOperator_Float_GTE = 10, // Field as a float is greater than or equal value - k_EOperator_Float_Not_GTE = 11, // Field as a float is not greater than or equal value - k_EOperator_Subkey_Contains = 12, // Field contains value as a subkey - k_EOperator_Subkey_Not_Contains = 13, // Field does not contain value as a subkey - - // Must be last - k_EItemCriteriaOperator_Count = 14, -}; - //----------------------------------------------------------------------------- // Purpose: Possible positions to tell the overlay to show notifications in //----------------------------------------------------------------------------- @@ -270,10 +221,10 @@ public: //----------------------------------------------------------------------------- CSteamID() { - m_unAccountID = 0; - m_EAccountType = k_EAccountTypeInvalid; - m_EUniverse = k_EUniverseInvalid; - m_unAccountInstance = 0; + m_steamid.m_comp.m_unAccountID = 0; + m_steamid.m_comp.m_EAccountType = k_EAccountTypeInvalid; + m_steamid.m_comp.m_EUniverse = k_EUniverseInvalid; + m_steamid.m_comp.m_unAccountInstance = 0; } @@ -325,10 +276,18 @@ public: //----------------------------------------------------------------------------- void Set( uint32 unAccountID, EUniverse eUniverse, EAccountType eAccountType ) { - m_unAccountID = unAccountID; - m_EUniverse = eUniverse; - m_EAccountType = eAccountType; - m_unAccountInstance = 1; + m_steamid.m_comp.m_unAccountID = unAccountID; + m_steamid.m_comp.m_EUniverse = eUniverse; + m_steamid.m_comp.m_EAccountType = eAccountType; + + if ( eAccountType == k_EAccountTypeClan ) + { + m_steamid.m_comp.m_unAccountInstance = 0; + } + else + { + m_steamid.m_comp.m_unAccountInstance = 1; + } } @@ -340,10 +299,10 @@ public: //----------------------------------------------------------------------------- void InstancedSet( uint32 unAccountID, uint32 unInstance, EUniverse eUniverse, EAccountType eAccountType ) { - m_unAccountID = unAccountID; - m_EUniverse = eUniverse; - m_EAccountType = eAccountType; - m_unAccountInstance = unInstance; + m_steamid.m_comp.m_unAccountID = unAccountID; + m_steamid.m_comp.m_EUniverse = eUniverse; + m_steamid.m_comp.m_EAccountType = eAccountType; + m_steamid.m_comp.m_unAccountInstance = unInstance; } @@ -353,10 +312,10 @@ public: //----------------------------------------------------------------------------- void FullSet( uint64 ulIdentifier, EUniverse eUniverse, EAccountType eAccountType ) { - m_unAccountID = ( ulIdentifier & 0xFFFFFFFF ); // account ID is low 32 bits - m_unAccountInstance = ( ( ulIdentifier >> 32 ) & 0xFFFFF ); // account instance is next 20 bits - m_EUniverse = eUniverse; - m_EAccountType = eAccountType; + m_steamid.m_comp.m_unAccountID = ( ulIdentifier & 0xFFFFFFFF ); // account ID is low 32 bits + m_steamid.m_comp.m_unAccountInstance = ( ( ulIdentifier >> 32 ) & 0xFFFFF ); // account instance is next 20 bits + m_steamid.m_comp.m_EUniverse = eUniverse; + m_steamid.m_comp.m_EAccountType = eAccountType; } @@ -366,11 +325,7 @@ public: //----------------------------------------------------------------------------- void SetFromUint64( uint64 ulSteamID ) { - m_unAccountID = ( ulSteamID & 0xFFFFFFFF ); // account ID is low 32 bits - m_unAccountInstance = ( ( ulSteamID >> 32 ) & 0xFFFFF ); // account instance is next 20 bits - - m_EAccountType = ( EAccountType ) ( ( ulSteamID >> 52 ) & 0xF ); // type is next 4 bits - m_EUniverse = ( EUniverse ) ( ( ulSteamID >> 56 ) & 0xFF ); // universe is next 8 bits + m_steamid.m_unAll64Bits = ulSteamID; } @@ -382,11 +337,11 @@ public: //----------------------------------------------------------------------------- void SetFromSteam2( TSteamGlobalUserID *pTSteamGlobalUserID, EUniverse eUniverse ) { - m_unAccountID = pTSteamGlobalUserID->m_SteamLocalUserID.Split.Low32bits * 2 + + m_steamid.m_comp.m_unAccountID = pTSteamGlobalUserID->m_SteamLocalUserID.Split.Low32bits * 2 + pTSteamGlobalUserID->m_SteamLocalUserID.Split.High32bits; - m_EUniverse = eUniverse; // set the universe - m_EAccountType = k_EAccountTypeIndividual; // Steam 2 accounts always map to account type of individual - m_unAccountInstance = 1; // individual accounts always have an account instance ID of 1 + m_steamid.m_comp.m_EUniverse = eUniverse; // set the universe + m_steamid.m_comp.m_EAccountType = k_EAccountTypeIndividual; // Steam 2 accounts always map to account type of individual + m_steamid.m_comp.m_unAccountInstance = 1; // individual accounts always have an account instance ID of 1 } //----------------------------------------------------------------------------- @@ -396,11 +351,11 @@ public: void ConvertToSteam2( TSteamGlobalUserID *pTSteamGlobalUserID ) const { // only individual accounts have any meaning in Steam 2, only they can be mapped - // Assert( m_EAccountType == k_EAccountTypeIndividual ); + // Assert( m_steamid.m_comp.m_EAccountType == k_EAccountTypeIndividual ); pTSteamGlobalUserID->m_SteamInstanceID = 0; - pTSteamGlobalUserID->m_SteamLocalUserID.Split.High32bits = m_unAccountID % 2; - pTSteamGlobalUserID->m_SteamLocalUserID.Split.Low32bits = m_unAccountID / 2; + pTSteamGlobalUserID->m_SteamLocalUserID.Split.High32bits = m_steamid.m_comp.m_unAccountID % 2; + pTSteamGlobalUserID->m_SteamLocalUserID.Split.Low32bits = m_steamid.m_comp.m_unAccountID / 2; } #endif // defined( INCLUDED_STEAM_COMMON_STEAMCOMMON_H ) @@ -410,8 +365,7 @@ public: //----------------------------------------------------------------------------- uint64 ConvertToUint64() const { - return (uint64) ( ( ( (uint64) m_EUniverse ) << 56 ) + ( ( (uint64) m_EAccountType ) << 52 ) + - ( ( (uint64) m_unAccountInstance ) << 32 ) + m_unAccountID ); + return m_steamid.m_unAll64Bits; } @@ -425,7 +379,7 @@ public: uint64 GetStaticAccountKey() const { // note we do NOT include the account instance (which is a dynamic property) in the static account key - return (uint64) ( ( ( (uint64) m_EUniverse ) << 56 ) + ((uint64) m_EAccountType << 52 ) + m_unAccountID ); + return (uint64) ( ( ( (uint64) m_steamid.m_comp.m_EUniverse ) << 56 ) + ((uint64) m_steamid.m_comp.m_EAccountType << 52 ) + m_steamid.m_comp.m_unAccountID ); } @@ -434,10 +388,10 @@ public: //----------------------------------------------------------------------------- void CreateBlankAnonLogon( EUniverse eUniverse ) { - m_unAccountID = 0; - m_EAccountType = k_EAccountTypeAnonGameServer; - m_EUniverse = eUniverse; - m_unAccountInstance = 0; + m_steamid.m_comp.m_unAccountID = 0; + m_steamid.m_comp.m_EAccountType = k_EAccountTypeAnonGameServer; + m_steamid.m_comp.m_EUniverse = eUniverse; + m_steamid.m_comp.m_unAccountInstance = 0; } @@ -446,10 +400,10 @@ public: //----------------------------------------------------------------------------- void CreateBlankAnonUserLogon( EUniverse eUniverse ) { - m_unAccountID = 0; - m_EAccountType = k_EAccountTypeAnonUser; - m_EUniverse = eUniverse; - m_unAccountInstance = 0; + m_steamid.m_comp.m_unAccountID = 0; + m_steamid.m_comp.m_EAccountType = k_EAccountTypeAnonUser; + m_steamid.m_comp.m_EUniverse = eUniverse; + m_steamid.m_comp.m_unAccountInstance = 0; } //----------------------------------------------------------------------------- @@ -457,7 +411,7 @@ public: //----------------------------------------------------------------------------- bool BBlankAnonAccount() const { - return m_unAccountID == 0 && BAnonAccount() && m_unAccountInstance == 0; + return m_steamid.m_comp.m_unAccountID == 0 && BAnonAccount() && m_steamid.m_comp.m_unAccountInstance == 0; } //----------------------------------------------------------------------------- @@ -465,7 +419,7 @@ public: //----------------------------------------------------------------------------- bool BGameServerAccount() const { - return m_EAccountType == k_EAccountTypeGameServer || m_EAccountType == k_EAccountTypeAnonGameServer; + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeGameServer || m_steamid.m_comp.m_EAccountType == k_EAccountTypeAnonGameServer; } //----------------------------------------------------------------------------- @@ -473,7 +427,7 @@ public: //----------------------------------------------------------------------------- bool BContentServerAccount() const { - return m_EAccountType == k_EAccountTypeContentServer; + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeContentServer; } @@ -482,7 +436,7 @@ public: //----------------------------------------------------------------------------- bool BClanAccount() const { - return m_EAccountType == k_EAccountTypeClan; + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeClan; } @@ -491,7 +445,16 @@ public: //----------------------------------------------------------------------------- bool BChatAccount() const { - return m_EAccountType == k_EAccountTypeChat; + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeChat; + } + + //----------------------------------------------------------------------------- + // Purpose: Is this a chat account id? + //----------------------------------------------------------------------------- + bool IsLobby() const + { + return ( m_steamid.m_comp.m_EAccountType == k_EAccountTypeChat ) + && ( m_steamid.m_comp.m_unAccountInstance & k_EChatInstanceFlagLobby ); } @@ -500,7 +463,7 @@ public: //----------------------------------------------------------------------------- bool BIndividualAccount() const { - return m_EAccountType == k_EAccountTypeIndividual; + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeIndividual; } @@ -509,36 +472,39 @@ public: //----------------------------------------------------------------------------- bool BAnonAccount() const { - return m_EAccountType == k_EAccountTypeAnonUser || m_EAccountType == k_EAccountTypeAnonGameServer; + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeAnonUser || m_steamid.m_comp.m_EAccountType == k_EAccountTypeAnonGameServer; + } + + //----------------------------------------------------------------------------- + // Purpose: Is this an anonymous user account? ( used to create an account or reset a password ) + //----------------------------------------------------------------------------- + bool BAnonUserAccount() const + { + return m_steamid.m_comp.m_EAccountType == k_EAccountTypeAnonUser; } // simple accessors - void SetAccountID( uint32 unAccountID ) { m_unAccountID = unAccountID; } - uint32 GetAccountID() const { return m_unAccountID; } - uint32 GetUnAccountInstance() const { return m_unAccountInstance; } - EAccountType GetEAccountType() const { return ( EAccountType ) m_EAccountType; } - EUniverse GetEUniverse() const { return m_EUniverse; } - void SetEUniverse( EUniverse eUniverse ) { m_EUniverse = eUniverse; } - bool IsValid() const { return ( m_EAccountType != k_EAccountTypeInvalid && m_EUniverse != k_EUniverseInvalid ); } + void SetAccountID( uint32 unAccountID ) { m_steamid.m_comp.m_unAccountID = unAccountID; } + uint32 GetAccountID() const { return m_steamid.m_comp.m_unAccountID; } + uint32 GetUnAccountInstance() const { return m_steamid.m_comp.m_unAccountInstance; } + EAccountType GetEAccountType() const { return ( EAccountType ) m_steamid.m_comp.m_EAccountType; } + EUniverse GetEUniverse() const { return m_steamid.m_comp.m_EUniverse; } + void SetEUniverse( EUniverse eUniverse ) { m_steamid.m_comp.m_EUniverse = eUniverse; } + bool IsValid() const { return ( m_steamid.m_comp.m_EAccountType != k_EAccountTypeInvalid && m_steamid.m_comp.m_EUniverse != k_EUniverseInvalid ); } // this set of functions is hidden, will be moved out of class explicit CSteamID( const char *pchSteamID, EUniverse eDefaultUniverse = k_EUniverseInvalid ); - char * Render() const; // renders this steam ID to string - static char * Render( uint64 ulSteamID ); // static method to render a uint64 representation of a steam ID to a string + const char * Render() const; // renders this steam ID to string + static const char * Render( uint64 ulSteamID ); // static method to render a uint64 representation of a steam ID to a string void SetFromString( const char *pchSteamID, EUniverse eDefaultUniverse ); bool SetFromSteam2String( const char *pchSteam2ID, EUniverse eUniverse ); - bool operator==( const CSteamID &val ) const - { - return ( ( val.m_unAccountID == m_unAccountID ) && ( val.m_unAccountInstance == m_unAccountInstance ) - && ( val.m_EAccountType == m_EAccountType ) && ( val.m_EUniverse == m_EUniverse ) ); - } - - bool operator!=( const CSteamID &val ) const { return !operator==( val ); } - bool operator<( const CSteamID &val ) const { return ConvertToUint64() < val.ConvertToUint64(); } - bool operator>( const CSteamID &val ) const { return ConvertToUint64() > val.ConvertToUint64(); } + inline bool operator==( const CSteamID &val ) const { return m_steamid.m_unAll64Bits == val.m_steamid.m_unAll64Bits; } + inline bool operator!=( const CSteamID &val ) const { return !operator==( val ); } + inline bool operator<( const CSteamID &val ) const { return m_steamid.m_unAll64Bits < val.m_steamid.m_unAll64Bits; } + inline bool operator>( const CSteamID &val ) const { return m_steamid.m_unAll64Bits > val.m_steamid.m_unAll64Bits; } // DEBUG function bool BValidExternalSteamID() const; @@ -551,45 +517,21 @@ private: CSteamID( uint32 ); CSteamID( int32 ); -#ifdef _WIN32 -#pragma warning(push) -#pragma warning(disable:4201) // nameless union is nonstandard // 64 bits total - union + union SteamID_t { - struct + struct SteamIDComponent_t { -#endif uint32 m_unAccountID : 32; // unique account identifier unsigned int m_unAccountInstance : 20; // dynamic instance ID (used for multiseat type accounts only) unsigned int m_EAccountType : 4; // type of account - can't show as EAccountType, due to signed / unsigned difference - EUniverse m_EUniverse : 8; // universe this account belongs to -#ifdef _WIN32 - }; + EUniverse m_EUniverse : 8; // universe this account belongs to + } m_comp; uint64 m_unAll64Bits; - }; -#pragma warning(pop) // no more anonymous unions until next time -#endif + } m_steamid; }; -const int k_unSteamAccountIDMask = 0xFFFFFFFF; -const int k_unSteamAccountInstanceMask = 0x000FFFFF; - - -// Special flags for Chat accounts - they go in the top 8 bits -// of the steam ID's "instance", leaving 12 for the actual instances -enum EChatSteamIDInstanceFlags -{ - k_EChatAccountInstanceMask = 0x00000FFF, // top 8 bits are flags - - k_EChatInstanceFlagClan = ( k_unSteamAccountInstanceMask + 1 ) >> 1, // top bit - k_EChatInstanceFlagLobby = ( k_unSteamAccountInstanceMask + 1 ) >> 2, // next one down, etc - - // Max of 8 flags -}; - - // generic invalid CSteamID const CSteamID k_steamIDNil; @@ -609,14 +551,14 @@ const CSteamID k_steamIDNonSteamGS( 2, 0, k_EUniverseInvalid, k_EAccountTypeInva #ifdef STEAM // Returns the matching chat steamID, with the default instance of 0 // If the steamID passed in is already of type k_EAccountTypeChat it will be returned with the same instance -CSteamID ChatIDFromSteamID( CSteamID &steamID ); +CSteamID ChatIDFromSteamID( const CSteamID &steamID ); // Returns the matching clan steamID, with the default instance of 0 // If the steamID passed in is already of type k_EAccountTypeClan it will be returned with the same instance -CSteamID ClanIDFromSteamID( CSteamID &steamID ); +CSteamID ClanIDFromSteamID( const CSteamID &steamID ); // Asserts steamID type before conversion -CSteamID ChatIDFromClanID( CSteamID &steamIDClan ); +CSteamID ChatIDFromClanID( const CSteamID &steamIDClan ); // Asserts steamID type before conversion -CSteamID ClanIDFromChatID( CSteamID &steamIDChat ); +CSteamID ClanIDFromChatID( const CSteamID &steamIDChat ); #endif // _STEAM @@ -662,8 +604,8 @@ public: // Hidden functions used only by Steam explicit CGameID( const char *pchGameID ); - char *Render() const; // render this Game ID to string - static char *Render( uint64 ulGameID ); // static method to render a uint64 representation of a Game ID to a string + const char *Render() const; // render this Game ID to string + static const char *Render( uint64 ulGameID ); // static method to render a uint64 representation of a Game ID to a string // must include checksum_crc.h first to get this functionality #if defined( CHECKSUM_CRC_H ) diff --git a/public/steam/steamtypes.h b/public/steam/steamtypes.h index dd0e123..10b689f 100644 --- a/public/steam/steamtypes.h +++ b/public/steam/steamtypes.h @@ -81,4 +81,10 @@ typedef uint32 RTime32; typedef uint32 CellID_t; +// handle to a Steam API call +typedef uint64 SteamAPICall_t; + + + + #endif // STEAMTYPES_H diff --git a/redistributable_bin/SteamAPIUpdater.dll b/redistributable_bin/SteamAPIUpdater.dll index 0ae0a696cfbad6b47e92f6cdc52a8b4ce4052ec2..1966171b9e102c36d4f950a779a4f7842156bcae 100644 GIT binary patch delta 707 zcmew`+5f|2{|P?zYtH3+I)v}|C!V}w&zbTY%UNDN-8=Qx@x9{fzQ=A^vh&lW5RD3x z<%c`v)h{y~+TF2wapMv018P>SYxGz$PaM1|v}s%TR(Dm~V>y}~7d6gLn=$itZvMjb zrA;fIM!1}L6|gAI#c4}z$zekix2e}p#Z9}-v^HI#i#EwtF#xFcT0n12GE_vjQ<25VHd@2M}`tF&7YX12GQ}^8zs+5c31E z01yiTvCwueM&X^CMV_;9YPET^edlFlXIjuCa>JlWp|n=Lk~8sJAJ@@j-g>Jt!~^7{WfJ;oxNXa(s>+;VCca)VA3 z{4xShE0q) zK%tE3-J6ATc{tfvwfUHtq*xgi#7_TzLRhEX(!kKr%)r3F$kZsxKpCNt#ZZhzM98RC zx$>i2e~GVZ&^r4ydm3D$gbaA$iWnJL8CV({OnSTfJo^kB^_y=d(K{ljUk= z^R;~(ZFb=QF~#T3_g)L-t@2oHQ>*9yFC_goXG*r!Esso_hPuORpXWzy$T5;l)cm_l NucbvLeh&i(006q&Ab9`) delta 630 zcmew`+5f|2{|P?z!FdNfny%lO>afnXY}*sN`+ICS&e)utB)HW4UApHNzmzLMZ6Eq0 zwPKy)oX@}8GmSOe$TBk0ZgR=~hjZK9*Dh+?lO|$4C92pfv;B_#e|d(SnYM?P-}!EM zS$@@?81{>ge!1Cwd%EM?HD$M*XPNI_^_1Ni6|q9utNB@M*=yHzlcQK~vKn$%PM^4f zPhsMR2&UEX&0dV{UW_2j1jNih%mT!$K+Fcj>_E%`#GF9P1;pGy%mc){K+Ffk{6H)K z#DYL9wB3tQc<1Kn+zW)OM2h0NCH#(QxIXt=u-xK6)P{9`r#{<0ajWoUf$cdPg>@K3 zjxJieR_OGaSFArAPv}Pkuyq{nyfb~xCSjNQD3%)s!wa&Gsd;2C5;$2IwfXnWio9et z#@NtDTjrc}Ie6uEqwW6{QAW1RSxqb7aVA_iQ2B6YMr-@S2aze7tijex&qf>w7ScFcfWpD z&UMwf%Nf^9FW4-6wBFLd#K_pl)WAH-K%R|LtIebBJ1-+Ai=hyUfNs9`hb5;YuGNR$ zaJ#nUO0@wmTmd5^D+5bo!xZ18kH1?>ert`nY&T1^#jJPZ(phUw4;++C`O(lkfz?L* zj8HKD{#Dy0cva16mS2DIbJ5{E5x*uysoOrUT*D6BR8n`T&1kM$kP~~vE1bRLHRr5N zjantAt}7q;kgVIvv(WvzUCO!llUF4!Ty~JxgQ3K{#s71ifX<_k+?_^T>sXyc8NdJl Dl5-Ad diff --git a/redistributable_bin/UpdateDLLWrapper.dll b/redistributable_bin/UpdateDLLWrapper.dll index 258d90cd9a22d5a69d67f97e235e9186eb1f2e49..c0c4b5df1e1c6263f2264656e31c24baa7963aaa 100644 GIT binary patch delta 623 zcmV-#0+9Xq$piSw1CUIAs?KjoGYkH|GC$PD=h=L=I*?`0m;r3=LQKXhDe6@4FLxNpn??GFoG1+lOND#6fLJ|%qb=7CI2(4? zb+eh!(+~xQFoFYuvvJ`k0Tiw|s0srEAD&LNQUpHQF*YzTFfuhUS}-F92`Yw2hW8Bt2?Q|~1Qg_@JgLbZcD$Hn zK?i-8u7@x-%sVg*1_>&LNQUlxoP5p z{v&o>1#dRWHZ9YnW2E&gZYI3KY1nE20#4nXLZ?MVi+eOM>?>VT@Gjc5c(#6O(<`E+ Jp8x;=000qo6T$!h delta 623 zcmexylKIC;<_SLaL3sx}93HJR<2d#Gso!6dd3&a+gt@uApD&)5f9#=|aM+xQ^TVz@ zX`QtEfr8R@sgJL}%W;OuoppTo%wh4AtylhRy1n+&0S2R0Uc#rUetawEG&t8(|p znV1m2-|PArbEBPmCrBF__N_j|L*M*rO$J*cJ51dn>kTY zV&aPkCO^&1PK+ndi9BQD)N1o+`_9YA&a|LOt`fQ{QaCw?DqEkuO|LJexaE^vcW_~ChluntoQOhmO~tKzDx+L54-d8+luM$ zyb=YI8*k}_Xg$!M?`WB8`1acDL&;y<<_q1(&yPE`S@Hh*rUT9YvNLUxE7$sjf1i44 z)2-(V3vM2{b!DsK?m|niwj-hQ_UNB}AZQYsWT+(Lw6KY>&Y+303hq5lHp3>yY@ks3 zr{2QA(!j*X*vQntEXqJ0p_9c>h(#b_-ahuTPfq-D zuDluC6)aX_zzbKv$jHjT(%9gfSt9)G(xZy+6}ln;%>U+Hk-qvs`GC-a6LZ$;%-CO+ z_c(EN+2MBcHa_E-oqiqhG3{@|F1)mklK;gbCHmr$E}NV1Wev9NmiH^J*==61=+W*6 zcklgMyXi^T)aT{NYE$+(M3$XsJ$~lbpVSAhCQXzHcbocpyKME`IeS|kEq-ue+Ttnh H3}65N<`yBA diff --git a/redistributable_bin/steam_api.dll b/redistributable_bin/steam_api.dll index 5ebf334808fe004a73974b84cc274d467ee305ff..4ae07ef0cd411ebec80b00a287e8409301f8fec8 100644 GIT binary patch delta 35347 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 afnd8+Aq>9*O-=K$i}(e^bAAS5Nbm=7$Nyvi literal 9100 zcmcIpOK%iM5H7C+gayLl_X~S%oB&A#w%K(+i6WbIfE8=7ymrC?X;{{Sy|6FsBRS*{ zi9=42rThWJkrO`v<=_M2h;qn*11E&|5nM=h_dL3Kx`*A2Gg8f?x~jkK>Z6F+qbb0XSj3gGj>*f@U58a6;J`q~WK6hE@O=4ZIUH zw1?kFBZ$XnwCvDZd?Jm#cIY-fk;b13nn(dKntCW`65GRQ3j4}v68pqx;)$S>j{!I( z@tL%&?CRRh^$ly~hkRx|yS8z2-Dj;B2#Jkrn>VgoE6W?p zVC9+mX47tkI(4yk3&kmPME!XQ*Krncjwcf5~3gz26jVfkFjUC&Klubw@Q@Mf7`xqJ5=Z zEvo%A$cP%Q+O2E+jafC*ZZuSvvGfvN`+sVbTJ~zGT3TsW>Vb|crJh#aZn-*{TBV|b zGMni{JpF97L_M2n2&N~K-)XnDYQIzijq(e)Y^H|RtG0ye5n-hyDD8xUG+snSjuukj zG}WXh_k`;CO*iWLBEj~Wy_>?&sNwZoEmUlciDb4S7c`D9EL&!^&_wSQb#KyYPWmin9vV=xE zyOv>-RHoKeRt5J8&na2*DDSbEh$gd>L zuuzkhB1uEjH>p(Vn0eZh@+GI25=YFVm;>*KrH-aP?;^1UvCne5^nG}O-AkkJw z%HK0kG2aa(ATP=!p6|M($v!*SUcbzbJ8g~3A!+m(#kd=q7sNoOD7C@pF_Mhd9NO5F zS}4UMv5my{;#5)-(W)>s8=%0p%n-j)O~xoDGV=NoviEGdJJ)ktA5AUp?ox?7Qvl*}V9~l^$RIa6g>q5+p94D`9q6n8h8xxG))j zzmag;&aYm)FTvcNBe&?ValHqKA)YJ8_)C!7i{WzsS9f+DBb_<}{fR@!Wi_&kwe<9h zzSUB(QES%jwUTGn&n9!(8`pWMdOL~?rpamm%pKSpCPA2Kur~q(OT}8HjtxHj;#c_a z!9`>`>|!m9f!psTOnw+=$K~)IGNfLYlVUQ+@MY3Ihw*3F&AC`z0N*-a>=A<>@xK43 z6{OD{c+5iDJ!E1Y6XPE97jGYHC1^cnix>Gf$zv|KT;?%&KtnF(G0YPGhbMG0x|o)t z#SJGw@vbfB9nbl(_6iZiSnDkp5^HJKRLmn;tjU)YYj`pyE++Sn`{6wI+rph#6_fw4 zDvxiYD``^ZTd{1*=g5XVJwlJJOF>6E@# zY>hqaW0CVk3Q4{?uw&XV3!O&9xE@ht;RL9AtA^no(c_BkNJQL4!)Ttz{vI{plB_-| ze#pFZ%z(dNvqZ`esd$`)8LjKd?#_&MlzesKSBCY5t)j<0EPq=RHA>UY31L#1b7v7R zp~2HRao2qeiJcP~OmB-yfVz*JFdy#I;i`MzsQB*YOR@ErXzP9hexB{{a8pg8$yX;{ z6*9^LI%K|CCn&T99bhgR=H#!jw}U#&km-W7q!VoSSGZ#7Gl)H;!!}G6`-wa3;pz@8 z2vXO!P8&QciTxdsaQ*+p5%EGIcvOey9}AZu<)=3hk