using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Facepunch.Steamworks.Data; namespace Facepunch.Steamworks { /// /// Functions for accessing and manipulating Steam user information. /// This is also where the APIs for Steam Voice are exposed. /// public class SteamUser : SteamClientClass { internal static ISteamUser Internal => Interface as ISteamUser; internal override void InitializeInterface( bool server ) { SetInterface( server, new ISteamUser( server ) ); InstallEvents(); richPresence = new Dictionary(); SampleRate = OptimalSampleRate; } static Dictionary richPresence; internal static void InstallEvents() { Dispatch.Install( x => OnSteamServersConnected?.Invoke() ); Dispatch.Install( x => OnSteamServerConnectFailure?.Invoke() ); Dispatch.Install( x => OnSteamServersDisconnected?.Invoke() ); Dispatch.Install( x => OnClientGameServerDeny?.Invoke() ); Dispatch.Install( x => OnLicensesUpdated?.Invoke() ); Dispatch.Install( x => OnValidateAuthTicketResponse?.Invoke( x.SteamID, x.OwnerSteamID, x.AuthSessionResponse ) ); Dispatch.Install( x => OnMicroTxnAuthorizationResponse?.Invoke( x.AppID, x.OrderID, x.Authorized != 0 ) ); Dispatch.Install( x => OnGameWebCallback?.Invoke( x.URLUTF8() ) ); Dispatch.Install( x => OnGetAuthSessionTicketResponse?.Invoke( x ) ); Dispatch.Install( x => OnDurationControl?.Invoke( new DurationControl { _inner = x } ) ); } /// /// Called when a connections to the Steam back-end has been established. /// This means the Steam client now has a working connection to the Steam servers. /// Usually this will have occurred before the game has launched, and should only be seen if the /// user has dropped connection due to a networking issue or a Steam server update. /// public static event Action OnSteamServersConnected; /// /// Called when a connection attempt has failed. /// This will occur periodically if the Steam client is not connected, /// and has failed when retrying to establish a connection. /// public static event Action OnSteamServerConnectFailure; /// /// Called if the client has lost connection to the Steam servers. /// Real-time services will be disabled until a matching OnSteamServersConnected has been posted. /// public static event Action OnSteamServersDisconnected; /// /// Sent by the Steam server to the client telling it to disconnect from the specified game server, /// which it may be in the process of or already connected to. /// The game client should immediately disconnect upon receiving this message. /// This can usually occur if the user doesn't have rights to play on the game server. /// public static event Action OnClientGameServerDeny; /// /// Called whenever the users licenses (owned packages) changes. /// public static event Action OnLicensesUpdated; /// /// Called when an auth ticket has been validated. /// The first parameter is the steamid of this user /// The second is the Steam ID that owns the game, this will be different from the first /// if the game is being borrowed via Steam Family Sharing /// public static event Action OnValidateAuthTicketResponse; /// /// Used internally for GetAuthSessionTicketAsync /// internal static event Action OnGetAuthSessionTicketResponse; /// /// Called when a user has responded to a microtransaction authorization request. /// ( appid, orderid, user authorized ) /// public static event Action OnMicroTxnAuthorizationResponse; /// /// Sent to your game in response to a steam://gamewebcallback/ command from a user clicking a link in the Steam overlay browser. /// You can use this to add support for external site signups where you want to pop back into the browser after some web page /// signup sequence, and optionally get back some detail about that. /// public static event Action OnGameWebCallback; /// /// Sent for games with enabled anti indulgence / duration control, for enabled users. /// Lets the game know whether persistent rewards or XP should be granted at normal rate, /// half rate, or zero rate. /// public static event Action OnDurationControl; static bool _recordingVoice; /// /// Starts/Stops voice recording. /// Once started, use GetAvailableVoice and GetVoice to get the data, and then call StopVoiceRecording /// when the user has released their push-to-talk hotkey or the game session has completed. /// public static bool VoiceRecord { get => _recordingVoice; set { _recordingVoice = value; if ( value ) Internal.StartVoiceRecording(); else Internal.StopVoiceRecording(); } } /// /// Returns true if we have voice data waiting to be read /// public static bool HasVoiceData { get { uint szCompressed = 0, deprecated = 0; if ( Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) return false; return szCompressed > 0; } } static byte[] readBuffer = new byte[1024*128]; /// /// Reads the voice data and returns the number of bytes written. /// The compressed data can be transmitted by your application and decoded back into raw audio data using /// DecompressVoice on the other side. The compressed data provided is in an arbitrary format and is not meant to be played directly. /// This should be called once per frame, and at worst no more than four times a second to keep the microphone input delay as low as /// possible. Calling this any less may result in gaps in the returned stream. /// public static unsafe int ReadVoiceData( System.IO.Stream stream ) { if ( !HasVoiceData ) return 0; uint szWritten = 0; uint deprecated = 0; fixed ( byte* b = readBuffer ) { if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return 0; } if ( szWritten == 0 ) return 0; stream.Write( readBuffer, 0, (int) szWritten ); return (int) szWritten; } /// /// Reads the voice data and returns the bytes. You should obviously ideally be using /// ReadVoiceData because it won't be creating a new byte array every call. But this /// makes it easier to get it working, so let the babies have their bottle. /// public static unsafe byte[] ReadVoiceDataBytes() { if ( !HasVoiceData ) return null; uint szWritten = 0; uint deprecated = 0; fixed ( byte* b = readBuffer ) { if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return null; } if ( szWritten == 0 ) return null; var arry = new byte[szWritten]; Array.Copy( readBuffer, 0, arry, 0, szWritten ); return arry; } static uint sampleRate = 48000; public static uint SampleRate { get => sampleRate; set { if ( SampleRate < 11025 ) throw new System.Exception( "Sample Rate must be between 11025 and 48000" ); if ( SampleRate > 48000 ) throw new System.Exception( "Sample Rate must be between 11025 and 48000" ); sampleRate = value; } } public static uint OptimalSampleRate => Internal.GetVoiceOptimalSampleRate(); /// /// Decodes the compressed voice data returned by GetVoice. /// The output data is raw single-channel 16-bit PCM audio.The decoder supports any sample rate from 11025 to 48000. /// public static unsafe int DecompressVoice( System.IO.Stream input, int length, System.IO.Stream output ) { var from = Helpers.TakeBuffer( length ); var to = Helpers.TakeBuffer( 1024 * 64 ); // // Copy from input stream to a pinnable buffer // using ( var s = new System.IO.MemoryStream( from ) ) { input.CopyTo( s ); } uint szWritten = 0; fixed ( byte* frm = from ) fixed ( byte* dst = to ) { if ( Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } if ( szWritten == 0 ) return 0; // // Copy to output buffer // output.Write( to, 0, (int)szWritten ); return (int)szWritten; } public static unsafe int DecompressVoice( byte[] from, System.IO.Stream output ) { var to = Helpers.TakeBuffer( 1024 * 64 ); uint szWritten = 0; fixed ( byte* frm = from ) fixed ( byte* dst = to ) { if ( Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } if ( szWritten == 0 ) return 0; // // Copy to output buffer // output.Write( to, 0, (int)szWritten ); return (int)szWritten; } /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// public static unsafe AuthTicket GetAuthSessionTicket() { var data = Helpers.TakeBuffer( 1024 ); fixed ( byte* b = data ) { uint ticketLength = 0; uint ticket = Internal.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ); if ( ticket == 0 ) return null; return new AuthTicket() { Data = data.Take( (int)ticketLength ).ToArray(), Handle = ticket }; } } /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// This waits for a positive response from the backend before returning the ticket. This means /// the ticket is definitely ready to go as soon as it returns. Will return null if the callback /// times out or returns negatively. /// public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) { var result = Result.Pending; AuthTicket ticket = null; var stopwatch = Stopwatch.StartNew(); void f( GetAuthSessionTicketResponse_t t ) { if ( t.AuthTicket != ticket.Handle ) return; result = t.Result; } OnGetAuthSessionTicketResponse += f; try { ticket = GetAuthSessionTicket(); if ( ticket == null ) return null; while ( result == Result.Pending ) { await Task.Delay( 10 ); if ( stopwatch.Elapsed.TotalSeconds > timeoutSeconds ) { ticket.Cancel(); return null; } } if ( result == Result.OK ) return ticket; ticket.Cancel(); return null; } finally { OnGetAuthSessionTicketResponse -= f; } } public static unsafe BeginAuthResult BeginAuthSession( byte[] ticketData, SteamId steamid ) { fixed ( byte* ptr = ticketData ) { return Internal.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ); } } public static void EndAuthSession( SteamId steamid ) => Internal.EndAuthSession( steamid ); // UserHasLicenseForApp - SERVER VERSION ( DLC CHECKING ) /// /// Checks if the current users looks like they are behind a NAT device. /// This is only valid if the user is connected to the Steam servers and may not catch all forms of NAT. /// public static bool IsBehindNAT => Internal.BIsBehindNAT(); /// /// Gets the Steam level of the user, as shown on their Steam community profile. /// public static int SteamLevel => Internal.GetPlayerSteamLevel(); /// /// Requests a URL which authenticates an in-game browser for store check-out, and then redirects to the specified URL. /// As long as the in-game browser accepts and handles session cookies, Steam microtransaction checkout pages will automatically recognize the user instead of presenting a login page. /// NOTE: The URL has a very short lifetime to prevent history-snooping attacks, so you should only call this API when you are about to launch the browser, or else immediately navigate to the result URL using a hidden browser window. /// NOTE: The resulting authorization cookie has an expiration time of one day, so it would be a good idea to request and visit a new auth URL every 12 hours. /// public static async Task GetStoreAuthUrlAsync( string url ) { var response = await Internal.RequestStoreAuthURL( url ); if ( !response.HasValue ) return null; return response.Value.URLUTF8(); } /// /// Checks whether the current user has verified their phone number. /// public static bool IsPhoneVerified => Internal.BIsPhoneVerified(); /// /// Checks whether the current user has Steam Guard two factor authentication enabled on their account. /// public static bool IsTwoFactorEnabled => Internal.BIsTwoFactorEnabled(); /// /// Checks whether the user's phone number is used to uniquely identify them. /// public static bool IsPhoneIdentifying => Internal.BIsPhoneIdentifying(); /// /// Checks whether the current user's phone number is awaiting (re)verification. /// public static bool IsPhoneRequiringVerification => Internal.BIsPhoneRequiringVerification(); /// /// Requests an application ticket encrypted with the secret "encrypted app ticket key". /// The encryption key can be obtained from the Encrypted App Ticket Key page on the App Admin for your app. /// There can only be one call pending, and this call is subject to a 60 second rate limit. /// If you get a null result from this it's probably because you're calling it too often. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) { var dataPtr = Marshal.AllocHGlobal( dataToInclude.Length ); Marshal.Copy( dataToInclude, 0, dataPtr, dataToInclude.Length ); try { var result = await Internal.RequestEncryptedAppTicket( dataPtr, dataToInclude.Length ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; byte[] data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { data = new byte[outSize]; Marshal.Copy( ticketData, data, 0, (int) outSize ); } Marshal.FreeHGlobal( ticketData ); return data; } finally { Marshal.FreeHGlobal( dataPtr ); } } /// /// Requests an application ticket encrypted with the secret "encrypted app ticket key". /// The encryption key can be obtained from the Encrypted App Ticket Key page on the App Admin for your app. /// There can only be one call pending, and this call is subject to a 60 second rate limit. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// public static async Task RequestEncryptedAppTicketAsync() { var result = await Internal.RequestEncryptedAppTicket( IntPtr.Zero, 0 ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; byte[] data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { data = new byte[outSize]; Marshal.Copy( ticketData, data, 0, (int)outSize ); } Marshal.FreeHGlobal( ticketData ); return data; } /// /// Get anti indulgence / duration control /// public static async Task GetDurationControl() { var response = await Internal.GetDurationControl(); if ( !response.HasValue ) return default; return new DurationControl { _inner = response.Value }; } } }