mirror of
https://github.com/Facepunch/Facepunch.Steamworks.git
synced 2025-01-28 14:38:15 +03:00
485 lines
16 KiB
C#
485 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Steamworks.Data;
|
|
|
|
namespace Steamworks
|
|
{
|
|
/// <summary>
|
|
/// Functions for accessing and manipulating Steam user information.
|
|
/// This is also where the APIs for Steam Voice are exposed.
|
|
/// </summary>
|
|
public static class SteamUser
|
|
{
|
|
static ISteamUser _internal;
|
|
internal static ISteamUser Internal
|
|
{
|
|
get
|
|
{
|
|
if ( _internal == null )
|
|
{
|
|
_internal = new ISteamUser();
|
|
_internal.Init();
|
|
|
|
richPresence = new Dictionary<string, string>();
|
|
|
|
SampleRate = OptimalSampleRate;
|
|
}
|
|
|
|
return _internal;
|
|
}
|
|
}
|
|
internal static void Shutdown()
|
|
{
|
|
_internal = null;
|
|
}
|
|
|
|
static Dictionary<string, string> richPresence;
|
|
|
|
internal static void InstallEvents()
|
|
{
|
|
SteamServersConnected_t.Install( x => OnSteamServersConnected?.Invoke() );
|
|
SteamServerConnectFailure_t.Install( x => OnSteamServerConnectFailure?.Invoke() );
|
|
SteamServersDisconnected_t.Install( x => OnSteamServersDisconnected?.Invoke() );
|
|
ClientGameServerDeny_t.Install( x => OnClientGameServerDeny?.Invoke() );
|
|
LicensesUpdated_t.Install( x => OnLicensesUpdated?.Invoke() );
|
|
ValidateAuthTicketResponse_t.Install( x => OnValidateAuthTicketResponse?.Invoke( x.SteamID, x.OwnerSteamID, x.AuthSessionResponse ) );
|
|
MicroTxnAuthorizationResponse_t.Install( x => OnMicroTxnAuthorizationResponse?.Invoke( x.AppID, x.OrderID, x.Authorized != 0 ) );
|
|
GameWebCallback_t.Install( x => OnGameWebCallback?.Invoke( x.URLUTF8() ) );
|
|
GetAuthSessionTicketResponse_t.Install( x => OnGetAuthSessionTicketResponse?.Invoke( x ) );
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static event Action OnSteamServersConnected;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static event Action OnSteamServerConnectFailure;
|
|
|
|
/// <summary>
|
|
/// Called if the client has lost connection to the Steam servers.
|
|
/// Real-time services will be disabled until a matching OnSteamServersConnected has been posted.
|
|
/// </summary>
|
|
public static event Action OnSteamServersDisconnected;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static event Action OnClientGameServerDeny;
|
|
|
|
/// <summary>
|
|
/// Called whenever the users licenses (owned packages) changes.
|
|
/// </summary>
|
|
public static event Action OnLicensesUpdated;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public static event Action<SteamId, SteamId, AuthResponse> OnValidateAuthTicketResponse;
|
|
|
|
/// <summary>
|
|
/// Used internally for GetAuthSessionTicketAsync
|
|
/// </summary>
|
|
internal static event Action<GetAuthSessionTicketResponse_t> OnGetAuthSessionTicketResponse;
|
|
|
|
/// <summary>
|
|
/// Called when a user has responded to a microtransaction authorization request.
|
|
/// ( appid, orderid, user authorized )
|
|
/// </summary>
|
|
public static event Action<AppId, ulong, bool> OnMicroTxnAuthorizationResponse;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static event Action<string> OnGameWebCallback;
|
|
|
|
|
|
|
|
|
|
static bool _recordingVoice;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
public static bool VoiceRecord
|
|
{
|
|
get => _recordingVoice;
|
|
set
|
|
{
|
|
_recordingVoice = value;
|
|
if ( value ) Internal.StartVoiceRecording();
|
|
else Internal.StopVoiceRecording();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Returns true if we have voice data waiting to be read
|
|
/// </summary>
|
|
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];
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you.
|
|
/// </summary>
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static async Task<AuthTicket> GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f )
|
|
{
|
|
var result = Result.Pending;
|
|
AuthTicket ticket = null;
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
Action<GetAuthSessionTicketResponse_t> f = ( 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 )
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static bool IsBehindNAT => Internal.BIsBehindNAT();
|
|
|
|
/// <summary>
|
|
/// Gets the Steam level of the user, as shown on their Steam community profile.
|
|
/// </summary>
|
|
public static int SteamLevel => Internal.GetPlayerSteamLevel();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static async Task<string> GetStoreAuthUrlAsync( string url )
|
|
{
|
|
var response = await Internal.RequestStoreAuthURL( url );
|
|
if ( !response.HasValue )
|
|
return null;
|
|
|
|
return response.Value.URLUTF8();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the current user has verified their phone number.
|
|
/// </summary>
|
|
public static bool IsPhoneVerified => Internal.BIsPhoneVerified();
|
|
|
|
/// <summary>
|
|
/// Checks whether the current user has Steam Guard two factor authentication enabled on their account.
|
|
/// </summary>
|
|
public static bool IsTwoFactorEnabled => Internal.BIsTwoFactorEnabled();
|
|
|
|
/// <summary>
|
|
/// Checks whether the user's phone number is used to uniquely identify them.
|
|
/// </summary>
|
|
public static bool IsPhoneIdentifying => Internal.BIsPhoneIdentifying();
|
|
|
|
/// <summary>
|
|
/// Checks whether the current user's phone number is awaiting (re)verification.
|
|
/// </summary>
|
|
public static bool IsPhoneRequiringVerification => Internal.BIsPhoneRequiringVerification();
|
|
|
|
/// <summary>
|
|
/// 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/
|
|
/// </summary>
|
|
public static async Task<byte[]> 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 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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/
|
|
/// </summary>
|
|
public static async Task<byte[]> 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;
|
|
|
|
}
|
|
|
|
}
|
|
} |