38f1ddb...178a853: v0.8.9.1, removed content folder
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class Achievements : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public Achievement[] All { get; private set; }
|
||||
|
||||
public event Action OnUpdated;
|
||||
public event Action<Achievement> OnAchievementStateChanged;
|
||||
|
||||
private List<Achievement> unlockedRecently = new List<Achievement>();
|
||||
|
||||
internal Achievements( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
All = new Achievement[0];
|
||||
c.RegisterCallback<UserStatsReceived_t>( UserStatsReceived );
|
||||
c.RegisterCallback<UserStatsStored_t>( UserStatsStored );
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
var old = All;
|
||||
|
||||
All = Enumerable.Range( 0, (int)client.native.userstats.GetNumAchievements() )
|
||||
.Select( x =>
|
||||
{
|
||||
if ( old != null )
|
||||
{
|
||||
var name = client.native.userstats.GetAchievementName( (uint)x );
|
||||
var found = old.FirstOrDefault( y => y.Id == name );
|
||||
if ( found != null )
|
||||
{
|
||||
if ( found.Refresh() )
|
||||
{
|
||||
unlockedRecently.Add( found );
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return new Achievement( client, x );
|
||||
} )
|
||||
.ToArray();
|
||||
|
||||
foreach ( var i in unlockedRecently )
|
||||
{
|
||||
OnUnlocked( i );
|
||||
}
|
||||
|
||||
unlockedRecently.Clear();
|
||||
}
|
||||
|
||||
internal void OnUnlocked( Achievement a )
|
||||
{
|
||||
OnAchievementStateChanged?.Invoke( a );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find an achievement by name. Will be null if not found, or not ready.
|
||||
/// </summary>
|
||||
public Achievement Find( string identifier )
|
||||
{
|
||||
return All.FirstOrDefault( x => x.Id == identifier );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlock an achievement by identifier. If apply is true this will happen as expected
|
||||
/// and the achievement overlay will popup etc. If it's false then you'll have to manually
|
||||
/// call Stats.StoreStats() to actually trigger it.
|
||||
/// </summary>
|
||||
public bool Trigger( string identifier, bool apply = true )
|
||||
{
|
||||
var a = Find( identifier );
|
||||
if ( a == null ) return false;
|
||||
|
||||
return a.Trigger( apply );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset an achievement by identifier
|
||||
/// </summary>
|
||||
public bool Reset( string identifier )
|
||||
{
|
||||
return client.native.userstats.ClearAchievement( identifier );
|
||||
}
|
||||
|
||||
private void UserStatsReceived( UserStatsReceived_t stats )
|
||||
{
|
||||
if ( stats.GameID != client.AppId ) return;
|
||||
|
||||
Refresh();
|
||||
|
||||
OnUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void UserStatsStored( UserStatsStored_t stats )
|
||||
{
|
||||
if ( stats.GameID != client.AppId ) return;
|
||||
|
||||
Refresh();
|
||||
|
||||
OnUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public class Achievement
|
||||
{
|
||||
private Client client;
|
||||
|
||||
public string Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if unlocked
|
||||
/// </summary>
|
||||
public bool State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should hold the unlock time if State is true
|
||||
/// </summary>
|
||||
public DateTime UnlockTime { get; private set; }
|
||||
|
||||
private int iconId { get; set; } = -1;
|
||||
private int refreshCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the percentage of users who have unlocked the specified achievement, or -1 if no data available.
|
||||
/// </summary>
|
||||
public float GlobalUnlockedPercentage
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( State )
|
||||
return 1;
|
||||
|
||||
float pct = 0;
|
||||
|
||||
if ( !client.native.userstats.GetAchievementAchievedPercent( Id, out pct ) )
|
||||
return -1.0f;
|
||||
|
||||
return pct;
|
||||
}
|
||||
}
|
||||
|
||||
private Image _icon;
|
||||
|
||||
public Image Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( iconId <= 0 ) return null;
|
||||
|
||||
if ( _icon == null )
|
||||
{
|
||||
_icon = new Image();
|
||||
_icon.Id = iconId;
|
||||
}
|
||||
|
||||
if ( _icon.IsLoaded )
|
||||
return _icon;
|
||||
|
||||
if ( !_icon.TryLoad( client.native.utils ) )
|
||||
return null;
|
||||
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
public Achievement( Client client, int index )
|
||||
{
|
||||
this.client = client;
|
||||
|
||||
Id = client.native.userstats.GetAchievementName( (uint) index );
|
||||
Name = client.native.userstats.GetAchievementDisplayAttribute( Id, "name" );
|
||||
Description = client.native.userstats.GetAchievementDisplayAttribute( Id, "desc" );
|
||||
|
||||
iconId = client.native.userstats.GetAchievementIcon( Id );
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make this achievement earned
|
||||
/// </summary>
|
||||
public bool Trigger( bool apply = true )
|
||||
{
|
||||
if ( State )
|
||||
return false;
|
||||
|
||||
State = true;
|
||||
UnlockTime = DateTime.Now;
|
||||
|
||||
var r = client.native.userstats.SetAchievement( Id );
|
||||
|
||||
if ( apply )
|
||||
{
|
||||
client.Stats.StoreStats();
|
||||
}
|
||||
|
||||
client.Achievements.OnUnlocked( this );
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset this achievement to not achieved
|
||||
/// </summary>
|
||||
public bool Reset()
|
||||
{
|
||||
State = false;
|
||||
UnlockTime = DateTime.Now;
|
||||
|
||||
return client.native.userstats.ClearAchievement( Id );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the unlock state. You shouldn't need to call this manually
|
||||
/// but it's here if you have to for some reason. Retuns true if state changed (meaning, probably unlocked)
|
||||
/// </summary>
|
||||
public bool Refresh()
|
||||
{
|
||||
bool previousState = State;
|
||||
|
||||
bool state = false;
|
||||
uint unlockTime;
|
||||
|
||||
State = false;
|
||||
|
||||
if ( client.native.userstats.GetAchievementAndUnlockTime( Id, ref state, out unlockTime ) )
|
||||
{
|
||||
State = state;
|
||||
UnlockTime = Utility.Epoch.ToDateTime( unlockTime );
|
||||
}
|
||||
|
||||
refreshCount++;
|
||||
|
||||
if ( previousState != State && refreshCount > 1 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class App : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
internal App( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
client.RegisterCallback<SteamNative.DlcInstalled_t>( DlcInstalled );
|
||||
}
|
||||
|
||||
public delegate void DlcInstalledDelegate( uint appid );
|
||||
|
||||
/// <summary>
|
||||
/// Triggered after the current user gains ownership of DLC and that DLC is installed.
|
||||
/// </summary>
|
||||
public event DlcInstalledDelegate OnDlcInstalled;
|
||||
|
||||
private void DlcInstalled( DlcInstalled_t data )
|
||||
{
|
||||
if ( OnDlcInstalled != null )
|
||||
{
|
||||
OnDlcInstalled( data.AppID );
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the content as corrupt, so it will validate the downloaded files
|
||||
/// once the app is closed. This is good to call when you detect a crash happening
|
||||
/// or a file is missing that is meant to be there.
|
||||
/// </summary>
|
||||
public void MarkContentCorrupt( bool missingFilesOnly = false )
|
||||
{
|
||||
client.native.apps.MarkContentCorrupt( missingFilesOnly );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell steam to install the Dlc specified by the AppId
|
||||
/// </summary>
|
||||
public void InstallDlc( uint appId )
|
||||
{
|
||||
client.native.apps.InstallDLC( appId );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell steam to uninstall the Dlc specified by the AppId
|
||||
/// </summary>
|
||||
public void UninstallDlc(uint appId)
|
||||
{
|
||||
client.native.apps.UninstallDLC( appId );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the purchase time for this appid. Will return DateTime.MinValue if none.
|
||||
/// </summary>
|
||||
public DateTime PurchaseTime(uint appId)
|
||||
{
|
||||
var time = client.native.apps.GetEarliestPurchaseUnixTime(appId);
|
||||
if ( time == 0 ) return DateTime.MinValue;
|
||||
|
||||
return Utility.Epoch.ToDateTime( time );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the active user is subscribed to a specified AppId.
|
||||
/// Only use this if you need to check ownership of another game related to yours, a demo for example.
|
||||
/// </summary>
|
||||
public bool IsSubscribed(uint appId)
|
||||
{
|
||||
return client.native.apps.BIsSubscribedApp(appId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if specified app is installed.
|
||||
/// </summary>
|
||||
public bool IsInstalled(uint appId)
|
||||
{
|
||||
return client.native.apps.BIsAppInstalled(appId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if specified app is installed.
|
||||
/// </summary>
|
||||
public bool IsDlcInstalled( uint appId )
|
||||
{
|
||||
return client.native.apps.BIsDlcInstalled( appId );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the appid's name
|
||||
/// Returns error if the current app Id does not have permission to use this interface
|
||||
/// </summary>
|
||||
public string GetName( uint appId )
|
||||
{
|
||||
var str = client.native.applist.GetAppName( appId );
|
||||
if ( str == null ) return "error";
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the app's install folder
|
||||
/// Returns error if the current app Id does not have permission to use this interface
|
||||
/// </summary>
|
||||
public string GetInstallFolder( uint appId )
|
||||
{
|
||||
return client.native.applist.GetAppInstallDir( appId );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the app's current build id
|
||||
/// Returns 0 if the current app Id does not have permission to use this interface
|
||||
/// </summary>
|
||||
public int GetBuildId( uint appId )
|
||||
{
|
||||
return client.native.applist.GetAppBuildId( appId );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class Client : IDisposable
|
||||
{
|
||||
Auth _auth;
|
||||
|
||||
public Auth Auth
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _auth == null )
|
||||
_auth = new Auth{ client = this };
|
||||
|
||||
return _auth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Auth
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public class Ticket : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public byte[] Data;
|
||||
public uint Handle;
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a ticket.
|
||||
/// You should cancel your ticket when you close the game or leave a server.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
if ( client.IsValid && Handle != 0 )
|
||||
{
|
||||
client.native.user.CancelAuthTicket( Handle );
|
||||
Handle = 0;
|
||||
Data = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an auth ticket.
|
||||
/// Which you can send to a server to authenticate that you are who you say you are.
|
||||
/// </summary>
|
||||
public unsafe Ticket GetAuthSessionTicket()
|
||||
{
|
||||
var data = new byte[1024];
|
||||
|
||||
fixed ( byte* b = data )
|
||||
{
|
||||
uint ticketLength = 0;
|
||||
uint ticket = client.native.user.GetAuthSessionTicket( (IntPtr) b, data.Length, out ticketLength );
|
||||
|
||||
if ( ticket == 0 )
|
||||
return null;
|
||||
|
||||
return new Ticket()
|
||||
{
|
||||
client = client,
|
||||
Data = data.Take( (int)ticketLength ).ToArray(),
|
||||
Handle = ticket
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class Client : IDisposable
|
||||
{
|
||||
Friends _friends;
|
||||
|
||||
public Friends Friends
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _friends == null )
|
||||
_friends = new Friends( this );
|
||||
|
||||
return _friends;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles most interactions with people in Steam, not just friends as the name would suggest.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// foreach ( var friend in client.Friends.AllFriends )
|
||||
/// {
|
||||
/// Console.WriteLine( $"{friend.Id}: {friend.Name}" );
|
||||
/// }
|
||||
/// </example>
|
||||
public class Friends
|
||||
{
|
||||
internal Client client;
|
||||
private byte[] buffer = new byte[1024 * 128];
|
||||
|
||||
internal Friends( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
client.RegisterCallback<AvatarImageLoaded_t>( OnAvatarImageLoaded );
|
||||
client.RegisterCallback<PersonaStateChange_t>( OnPersonaStateChange );
|
||||
client.RegisterCallback<GameRichPresenceJoinRequested_t>( OnGameJoinRequested );
|
||||
client.RegisterCallback<GameConnectedFriendChatMsg_t>( OnFriendChatMessage );
|
||||
}
|
||||
|
||||
public delegate void ChatMessageDelegate( SteamFriend friend, string type, string message );
|
||||
|
||||
/// <summary>
|
||||
/// Called when chat message has been received from a friend. You'll need to turn on
|
||||
/// ListenForFriendsMessages to recieve this.
|
||||
/// </summary>
|
||||
public event ChatMessageDelegate OnChatMessage;
|
||||
|
||||
private unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data )
|
||||
{
|
||||
if ( OnChatMessage == null ) return;
|
||||
|
||||
var friend = Get( data.SteamIDUser );
|
||||
var type = ChatEntryType.ChatMsg;
|
||||
fixed ( byte* ptr = buffer )
|
||||
{
|
||||
var len = client.native.friends.GetFriendMessage( data.SteamIDUser, data.MessageID, (IntPtr)ptr, buffer.Length, out type );
|
||||
|
||||
if ( len == 0 && type == ChatEntryType.Invalid )
|
||||
return;
|
||||
|
||||
var typeName = type.ToString();
|
||||
var message = Encoding.UTF8.GetString( buffer, 0, len );
|
||||
|
||||
OnChatMessage( friend, typeName, message );
|
||||
}
|
||||
}
|
||||
|
||||
private bool _listenForFriendsMessages;
|
||||
|
||||
/// <summary>
|
||||
/// Listens for Steam friends chat messages.
|
||||
/// You can then show these chats inline in the game. For example with a Blizzard style chat message system or the chat system in Dota 2.
|
||||
/// After enabling this you will receive callbacks when ever the user receives a chat message.
|
||||
/// </summary>
|
||||
public bool ListenForFriendsMessages
|
||||
{
|
||||
get
|
||||
{
|
||||
return _listenForFriendsMessages;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_listenForFriendsMessages = value;
|
||||
client.native.friends.SetListenForFriendsMessages( value );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public delegate void JoinRequestedDelegate( SteamFriend friend, string connect );
|
||||
|
||||
//
|
||||
// Called when a friend has invited you to their game (using InviteToGame)
|
||||
//
|
||||
public event JoinRequestedDelegate OnInvitedToGame;
|
||||
|
||||
|
||||
private void OnGameJoinRequested( GameRichPresenceJoinRequested_t data )
|
||||
{
|
||||
if ( OnInvitedToGame != null )
|
||||
{
|
||||
OnInvitedToGame( Get( data.SteamIDFriend ), data.Connect );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get information about this user - which as name and avatar.
|
||||
/// If returns true, we already have this user's information.
|
||||
/// </summary>
|
||||
public bool UpdateInformation( ulong steamid )
|
||||
{
|
||||
return !client.native.friends.RequestUserInformation( steamid, false );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get this user's name
|
||||
/// </summary>
|
||||
public string GetName( ulong steamid )
|
||||
{
|
||||
client.native.friends.RequestUserInformation( steamid, true );
|
||||
return client.native.friends.GetFriendPersonaName( steamid );
|
||||
}
|
||||
|
||||
private List<SteamFriend> _allFriends;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all friends, even blocked, ignored, friend requests etc
|
||||
/// </summary>
|
||||
public IEnumerable<SteamFriend> All
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _allFriends == null )
|
||||
{
|
||||
_allFriends = new List<SteamFriend>();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
return _allFriends;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns only friends
|
||||
/// </summary>
|
||||
public IEnumerable<SteamFriend> AllFriends
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach ( var friend in All )
|
||||
{
|
||||
if ( !friend.IsFriend ) continue;
|
||||
|
||||
yield return friend;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all blocked users
|
||||
/// </summary>
|
||||
public IEnumerable<SteamFriend> AllBlocked
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach ( var friend in All )
|
||||
{
|
||||
if ( !friend.IsBlocked ) continue;
|
||||
|
||||
yield return friend;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if ( _allFriends == null )
|
||||
{
|
||||
_allFriends = new List<SteamFriend>();
|
||||
}
|
||||
|
||||
_allFriends.Clear();
|
||||
|
||||
var flags = (int) SteamNative.FriendFlags.All;
|
||||
var count = client.native.friends.GetFriendCount( flags );
|
||||
|
||||
for ( int i=0; i<count; i++ )
|
||||
{
|
||||
var steamid = client.native.friends.GetFriendByIndex( i, flags );
|
||||
_allFriends.Add( Get( steamid ) );
|
||||
}
|
||||
}
|
||||
|
||||
public enum AvatarSize
|
||||
{
|
||||
/// <summary>
|
||||
/// Should be 32x32 - but make sure to check!
|
||||
/// </summary>
|
||||
Small,
|
||||
|
||||
/// <summary>
|
||||
/// Should be 64x64 - but make sure to check!
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// Should be 184x184 - but make sure to check!
|
||||
/// </summary>
|
||||
Large
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the avatar immediately. This should work for people on your friends list.
|
||||
/// </summary>
|
||||
public Image GetCachedAvatar( AvatarSize size, ulong steamid )
|
||||
{
|
||||
var imageid = 0;
|
||||
|
||||
switch (size)
|
||||
{
|
||||
case AvatarSize.Small:
|
||||
imageid = client.native.friends.GetSmallFriendAvatar(steamid);
|
||||
break;
|
||||
case AvatarSize.Medium:
|
||||
imageid = client.native.friends.GetMediumFriendAvatar(steamid);
|
||||
break;
|
||||
case AvatarSize.Large:
|
||||
imageid = client.native.friends.GetLargeFriendAvatar(steamid);
|
||||
break;
|
||||
}
|
||||
|
||||
if ( imageid == 1 ) return null; // Placeholder large
|
||||
if ( imageid == 2 ) return null; // Placeholder medium
|
||||
if ( imageid == 3 ) return null; // Placeholder small
|
||||
|
||||
var img = new Image()
|
||||
{
|
||||
Id = imageid
|
||||
};
|
||||
|
||||
if ( !img.TryLoad( client.native.utils ) )
|
||||
return null;
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Callback will be called when the avatar is ready. If we fail to get an
|
||||
/// avatar, might be called with a null Image.
|
||||
/// </summary>
|
||||
public void GetAvatar( AvatarSize size, ulong steamid, Action<Image> callback )
|
||||
{
|
||||
// Maybe we already have it downloaded?
|
||||
var image = GetCachedAvatar(size, steamid);
|
||||
if ( image != null )
|
||||
{
|
||||
callback(image);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lets request it from Steam
|
||||
if (!client.native.friends.RequestUserInformation(steamid, false))
|
||||
{
|
||||
// from docs: false means that we already have all the details about that user, and functions that require this information can be used immediately
|
||||
// but that's probably not true because we just checked the cache
|
||||
|
||||
// check again in case it was just a race
|
||||
image = GetCachedAvatar(size, steamid);
|
||||
if ( image != null )
|
||||
{
|
||||
callback(image);
|
||||
return;
|
||||
}
|
||||
|
||||
// maybe Steam returns false if it was already requested? just add another callback and hope it comes
|
||||
// if not it'll time out anyway
|
||||
}
|
||||
|
||||
PersonaCallbacks.Add( new PersonaCallback
|
||||
{
|
||||
SteamId = steamid,
|
||||
Size = size,
|
||||
Callback = callback,
|
||||
Time = DateTime.Now
|
||||
});
|
||||
}
|
||||
|
||||
private class PersonaCallback
|
||||
{
|
||||
public ulong SteamId;
|
||||
public AvatarSize Size;
|
||||
public Action<Image> Callback;
|
||||
public DateTime Time;
|
||||
}
|
||||
|
||||
List<PersonaCallback> PersonaCallbacks = new List<PersonaCallback>();
|
||||
|
||||
public SteamFriend Get( ulong steamid )
|
||||
{
|
||||
var friend = All.Where( x => x.Id == steamid ).FirstOrDefault();
|
||||
if ( friend != null ) return friend;
|
||||
|
||||
var f = new SteamFriend()
|
||||
{
|
||||
Id = steamid,
|
||||
Client = client
|
||||
};
|
||||
|
||||
f.Refresh();
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
internal void Cycle()
|
||||
{
|
||||
if ( PersonaCallbacks.Count == 0 ) return;
|
||||
|
||||
var timeOut = DateTime.Now.AddSeconds( -10 );
|
||||
|
||||
for ( int i = PersonaCallbacks.Count-1; i >= 0; i-- )
|
||||
{
|
||||
var cb = PersonaCallbacks[i];
|
||||
|
||||
// Timeout
|
||||
if ( cb.Time < timeOut )
|
||||
{
|
||||
if ( cb.Callback != null )
|
||||
{
|
||||
// final attempt, will be null unless the callback was missed somehow
|
||||
var image = GetCachedAvatar( cb.Size, cb.SteamId );
|
||||
|
||||
cb.Callback( image );
|
||||
}
|
||||
|
||||
PersonaCallbacks.Remove( cb );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnPersonaStateChange( PersonaStateChange_t data )
|
||||
{
|
||||
// k_EPersonaChangeAvatar
|
||||
if ( (data.ChangeFlags & 0x0040) == 0x0040 )
|
||||
{
|
||||
LoadAvatarForSteamId( data.SteamID );
|
||||
}
|
||||
|
||||
//
|
||||
// Find and refresh this friend's status
|
||||
//
|
||||
foreach ( var friend in All )
|
||||
{
|
||||
if ( friend.Id != data.SteamID ) continue;
|
||||
|
||||
friend.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void LoadAvatarForSteamId( ulong Steamid )
|
||||
{
|
||||
for ( int i = PersonaCallbacks.Count - 1; i >= 0; i-- )
|
||||
{
|
||||
var cb = PersonaCallbacks[i];
|
||||
if ( cb.SteamId != Steamid ) continue;
|
||||
|
||||
var image = GetCachedAvatar( cb.Size, cb.SteamId );
|
||||
if ( image == null ) continue;
|
||||
|
||||
PersonaCallbacks.Remove( cb );
|
||||
|
||||
if ( cb.Callback != null )
|
||||
{
|
||||
cb.Callback( image );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAvatarImageLoaded( AvatarImageLoaded_t data )
|
||||
{
|
||||
LoadAvatarForSteamId( data.SteamID );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class Image
|
||||
{
|
||||
public int Id { get; internal set; }
|
||||
public int Width { get; internal set; }
|
||||
public int Height { get; internal set; }
|
||||
|
||||
public byte[] Data { get; internal set; }
|
||||
|
||||
public bool IsLoaded { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return true if this image couldn't be loaded for some reason
|
||||
/// </summary>
|
||||
public bool IsError { get; internal set; }
|
||||
|
||||
unsafe internal bool TryLoad( SteamNative.SteamUtils utils )
|
||||
{
|
||||
if ( IsLoaded ) return true;
|
||||
|
||||
uint width = 0, height = 0;
|
||||
|
||||
if ( utils.GetImageSize( Id, out width, out height ) == false )
|
||||
{
|
||||
IsError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
var buffer = new byte[ width * height * 4 ];
|
||||
|
||||
fixed ( byte* ptr = buffer )
|
||||
{
|
||||
if ( utils.GetImageRGBA( Id, (IntPtr) ptr, buffer.Length ) == false )
|
||||
{
|
||||
IsError = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Width = (int) width;
|
||||
Height = (int) height;
|
||||
Data = buffer;
|
||||
IsLoaded = true;
|
||||
IsError = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Color GetPixel( int x, int y )
|
||||
{
|
||||
if ( !IsLoaded ) throw new System.Exception( "Image not loaded" );
|
||||
if ( x < 0 || x >= Width ) throw new System.Exception( "x out of bounds" );
|
||||
if ( y < 0 || y >= Height ) throw new System.Exception( "y out of bounds" );
|
||||
|
||||
Color c = new Color();
|
||||
|
||||
var i = ( y * Width + x ) * 4;
|
||||
|
||||
c.r = Data[i + 0];
|
||||
c.g = Data[i + 1];
|
||||
c.b = Data[i + 2];
|
||||
c.a = Data[i + 3];
|
||||
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
public struct Color
|
||||
{
|
||||
public byte r, g, b, a;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Facepunch.Steamworks.Callbacks;
|
||||
using SteamNative;
|
||||
using Result = SteamNative.Result;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class Leaderboard : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of leaderboard request
|
||||
/// </summary>
|
||||
public enum RequestType
|
||||
{
|
||||
/// <summary>
|
||||
/// Query everyone and everything
|
||||
/// </summary>
|
||||
Global = LeaderboardDataRequest.Global,
|
||||
|
||||
/// <summary>
|
||||
/// Query entries near to this user's rank
|
||||
/// </summary>
|
||||
GlobalAroundUser = LeaderboardDataRequest.GlobalAroundUser,
|
||||
|
||||
/// <summary>
|
||||
/// Only show friends of this user
|
||||
/// </summary>
|
||||
Friends = LeaderboardDataRequest.Friends
|
||||
}
|
||||
|
||||
private static readonly int[] subEntriesBuffer = new int[512];
|
||||
|
||||
internal ulong BoardId;
|
||||
public ulong GetBoardId()
|
||||
{
|
||||
return BoardId;
|
||||
}
|
||||
internal Client client;
|
||||
|
||||
private readonly Queue<Action> _onCreated = new Queue<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// The results from the last query. Can be null.
|
||||
/// </summary>
|
||||
public Entry[] Results;
|
||||
|
||||
internal Leaderboard( Client c )
|
||||
{
|
||||
client = c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of this board, as retrieved from Steam
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of entries on this board
|
||||
/// </summary>
|
||||
public int TotalEntries { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this board is valid, ie, we've received
|
||||
/// a positive response from Steam about it.
|
||||
/// </summary>
|
||||
public bool IsValid => BoardId != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we asked steam about this board but it returned
|
||||
/// an error.
|
||||
/// </summary>
|
||||
public bool IsError { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we're querying scores
|
||||
/// </summary>
|
||||
public bool IsQuerying { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
private void DispatchOnCreatedCallbacks()
|
||||
{
|
||||
while ( _onCreated.Count > 0 )
|
||||
{
|
||||
_onCreated.Dequeue()();
|
||||
}
|
||||
}
|
||||
|
||||
private bool DeferOnCreated( Action onValid, FailureCallback onFailure = null )
|
||||
{
|
||||
if ( IsValid || IsError ) return false;
|
||||
|
||||
_onCreated.Enqueue( () =>
|
||||
{
|
||||
if ( IsValid ) onValid();
|
||||
else onFailure?.Invoke( Callbacks.Result.Fail );
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the leaderboard information is successfully recieved from Steam
|
||||
/// </summary>
|
||||
public Action OnBoardInformation;
|
||||
|
||||
internal void OnBoardCreated( LeaderboardFindResult_t result, bool error )
|
||||
{
|
||||
Console.WriteLine( $"result.LeaderboardFound: {result.LeaderboardFound}" );
|
||||
Console.WriteLine( $"result.SteamLeaderboard: {result.SteamLeaderboard}" );
|
||||
|
||||
if ( error || ( result.LeaderboardFound == 0 ) )
|
||||
{
|
||||
IsError = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
BoardId = result.SteamLeaderboard;
|
||||
|
||||
if ( IsValid )
|
||||
{
|
||||
Name = client.native.userstats.GetLeaderboardName( BoardId );
|
||||
TotalEntries = client.native.userstats.GetLeaderboardEntryCount( BoardId );
|
||||
|
||||
OnBoardInformation?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
DispatchOnCreatedCallbacks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a score to this leaderboard.
|
||||
/// Subscores are totally optional, and can be used for other game defined data such as laps etc.. although
|
||||
/// they have no bearing on sorting at all
|
||||
/// If onlyIfBeatsOldScore is true, the score will only be updated if it beats the existing score, else it will always
|
||||
/// be updated. Beating the existing score is subjective - and depends on how your leaderboard was set up as to whether
|
||||
/// that means higher or lower.
|
||||
/// </summary>
|
||||
public bool AddScore( bool onlyIfBeatsOldScore, int score, params int[] subscores )
|
||||
{
|
||||
if ( IsError ) return false;
|
||||
if ( !IsValid ) return DeferOnCreated( () => AddScore( onlyIfBeatsOldScore, score, subscores ) );
|
||||
|
||||
var flags = LeaderboardUploadScoreMethod.ForceUpdate;
|
||||
if ( onlyIfBeatsOldScore ) flags = LeaderboardUploadScoreMethod.KeepBest;
|
||||
|
||||
client.native.userstats.UploadLeaderboardScore( BoardId, flags, score, subscores, subscores.Length );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by <see cref="AddScore(bool, int, int[], AddScoreCallback, FailureCallback)"/> when score submission
|
||||
/// is complete.
|
||||
/// </summary>
|
||||
/// <param name="result">If successful, information about the new entry</param>
|
||||
public delegate void AddScoreCallback( AddScoreResult result );
|
||||
|
||||
/// <summary>
|
||||
/// Information about a newly submitted score.
|
||||
/// </summary>
|
||||
public struct AddScoreResult
|
||||
{
|
||||
public int Score;
|
||||
public bool ScoreChanged;
|
||||
public int GlobalRankNew;
|
||||
public int GlobalRankPrevious;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a score to this leaderboard.
|
||||
/// Subscores are totally optional, and can be used for other game defined data such as laps etc.. although
|
||||
/// they have no bearing on sorting at all
|
||||
/// If onlyIfBeatsOldScore is true, the score will only be updated if it beats the existing score, else it will always
|
||||
/// be updated.
|
||||
/// Information about the newly submitted score is passed to the optional <paramref name="onSuccess"/>.
|
||||
/// </summary>
|
||||
public bool AddScore( bool onlyIfBeatsOldScore, int score, int[] subscores = null, AddScoreCallback onSuccess = null, FailureCallback onFailure = null )
|
||||
{
|
||||
if ( IsError ) return false;
|
||||
if ( !IsValid ) return DeferOnCreated( () => AddScore( onlyIfBeatsOldScore, score, subscores, onSuccess, onFailure ), onFailure );
|
||||
|
||||
if ( subscores == null ) subscores = new int[0];
|
||||
|
||||
var flags = LeaderboardUploadScoreMethod.ForceUpdate;
|
||||
if ( onlyIfBeatsOldScore ) flags = LeaderboardUploadScoreMethod.KeepBest;
|
||||
|
||||
client.native.userstats.UploadLeaderboardScore( BoardId, flags, score, subscores, subscores.Length, ( result, error ) =>
|
||||
{
|
||||
if ( !error && result.Success != 0 )
|
||||
{
|
||||
onSuccess?.Invoke( new AddScoreResult
|
||||
{
|
||||
Score = result.Score,
|
||||
ScoreChanged = result.ScoreChanged != 0,
|
||||
GlobalRankNew = result.GlobalRankNew,
|
||||
GlobalRankPrevious = result.GlobalRankPrevious
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
onFailure?.Invoke( error ? Callbacks.Result.IOFailure : Callbacks.Result.Fail );
|
||||
}
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by <see cref="Leaderboard.AttachRemoteFile"/> when file attachment is complete.
|
||||
/// </summary>
|
||||
public delegate void AttachRemoteFileCallback();
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to attach a <see cref="RemoteStorage"/> file to the current user's leaderboard entry.
|
||||
/// Can be useful for storing replays along with scores.
|
||||
/// </summary>
|
||||
/// <returns>True if the file attachment process has started</returns>
|
||||
public bool AttachRemoteFile( RemoteFile file, AttachRemoteFileCallback onSuccess = null, FailureCallback onFailure = null )
|
||||
{
|
||||
if ( IsError ) return false;
|
||||
if ( !IsValid ) return DeferOnCreated( () => AttachRemoteFile( file, onSuccess, onFailure ), onFailure );
|
||||
|
||||
if ( file.IsShared )
|
||||
{
|
||||
var handle = client.native.userstats.AttachLeaderboardUGC( BoardId, file.UGCHandle, ( result, error ) =>
|
||||
{
|
||||
if ( !error && result.Result == Result.OK )
|
||||
{
|
||||
onSuccess?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
onFailure?.Invoke( result.Result == 0 ? Callbacks.Result.IOFailure : (Callbacks.Result) result.Result );
|
||||
}
|
||||
} );
|
||||
|
||||
return handle.IsValid;
|
||||
}
|
||||
|
||||
file.Share( () =>
|
||||
{
|
||||
if ( !file.IsShared || !AttachRemoteFile( file, onSuccess, onFailure ) )
|
||||
{
|
||||
onFailure?.Invoke( Callbacks.Result.Fail );
|
||||
}
|
||||
}, onFailure );
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a subset of scores. The scores end up in Results.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if we have started the query</returns>
|
||||
public bool FetchScores( RequestType RequestType, int start, int end )
|
||||
{
|
||||
if ( !IsValid ) return false;
|
||||
if ( IsQuerying ) return false;
|
||||
|
||||
client.native.userstats.DownloadLeaderboardEntries( BoardId, (LeaderboardDataRequest) RequestType, start, end, OnScores );
|
||||
|
||||
Results = null;
|
||||
IsQuerying = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe void ReadScores( LeaderboardScoresDownloaded_t result, List<Entry> dest )
|
||||
{
|
||||
for ( var i = 0; i < result.CEntryCount; i++ )
|
||||
fixed ( int* ptr = subEntriesBuffer )
|
||||
{
|
||||
var entry = new LeaderboardEntry_t();
|
||||
if ( client.native.userstats.GetDownloadedLeaderboardEntry( result.SteamLeaderboardEntries, i, ref entry, (IntPtr) ptr, subEntriesBuffer.Length ) )
|
||||
dest.Add( new Entry
|
||||
{
|
||||
GlobalRank = entry.GlobalRank,
|
||||
Score = entry.Score,
|
||||
SteamId = entry.SteamIDUser,
|
||||
SubScores = entry.CDetails == 0 ? null : subEntriesBuffer.Take( entry.CDetails ).ToArray(),
|
||||
Name = client.Friends.GetName( entry.SteamIDUser ),
|
||||
AttachedFile = (entry.UGC >> 32) == 0xffffffff ? null : new RemoteFile( client.RemoteStorage, entry.UGC )
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic] private static List<Entry> _sEntryBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by <see cref="FetchScores(RequestType, int, int, FetchScoresCallback, FailureCallback)"/> when
|
||||
/// a query is complete.
|
||||
/// </summary>
|
||||
public delegate void FetchScoresCallback( Entry[] results );
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a subset of scores. The scores are passed to <paramref name="onSuccess"/>.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if we have started the query</returns>
|
||||
public bool FetchScores( RequestType RequestType, int start, int end, FetchScoresCallback onSuccess, FailureCallback onFailure = null )
|
||||
{
|
||||
if ( IsError ) return false;
|
||||
if ( !IsValid ) return DeferOnCreated( () => FetchScores( RequestType, start, end, onSuccess, onFailure ), onFailure );
|
||||
|
||||
client.native.userstats.DownloadLeaderboardEntries( BoardId, (LeaderboardDataRequest) RequestType, start, end, ( result, error ) =>
|
||||
{
|
||||
if ( error )
|
||||
{
|
||||
onFailure?.Invoke( Callbacks.Result.IOFailure );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( _sEntryBuffer == null ) _sEntryBuffer = new List<Entry>();
|
||||
else _sEntryBuffer.Clear();
|
||||
|
||||
ReadScores( result, _sEntryBuffer );
|
||||
onSuccess( _sEntryBuffer.ToArray() );
|
||||
}
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool FetchUsersScores( RequestType RequestType, UInt64[] steamIds, FetchScoresCallback onSuccess, FailureCallback onFailure = null )
|
||||
{
|
||||
|
||||
if ( IsError ) return false;
|
||||
if ( !IsValid ) return DeferOnCreated( () => FetchUsersScores( RequestType, steamIds, onSuccess, onFailure ), onFailure );
|
||||
|
||||
fixed(ulong* pointer = steamIds){
|
||||
|
||||
client.native.userstats.DownloadLeaderboardEntriesForUsers(BoardId, (IntPtr)pointer, steamIds.Length, (result, error) =>
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
onFailure?.Invoke(Callbacks.Result.IOFailure);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_sEntryBuffer == null) _sEntryBuffer = new List<Entry>();
|
||||
else _sEntryBuffer.Clear();
|
||||
|
||||
ReadScores(result, _sEntryBuffer);
|
||||
onSuccess(_sEntryBuffer.ToArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnScores( LeaderboardScoresDownloaded_t result, bool error )
|
||||
{
|
||||
IsQuerying = false;
|
||||
|
||||
if ( client == null ) return;
|
||||
if ( error ) return;
|
||||
|
||||
var list = new List<Entry>();
|
||||
ReadScores( result, list );
|
||||
|
||||
Results = list.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in a leaderboard
|
||||
/// </summary>
|
||||
public struct Entry
|
||||
{
|
||||
public ulong SteamId;
|
||||
public int Score;
|
||||
public int[] SubScores;
|
||||
public int GlobalRank;
|
||||
public RemoteFile AttachedFile;
|
||||
|
||||
/// <summary>
|
||||
/// Note that the player's name might not be immediately available.
|
||||
/// If that's the case you'll have to use Friends.GetName to find the name
|
||||
/// </summary>
|
||||
public string Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class Lobby
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to hold global lobby data. This is stuff like maps/modes/etc. Data set here can be filtered by LobbyList.
|
||||
/// </summary>
|
||||
public class LobbyData
|
||||
{
|
||||
internal Client client;
|
||||
internal ulong lobby;
|
||||
internal Dictionary<string, string> data;
|
||||
|
||||
public LobbyData( Client c, ulong l )
|
||||
{
|
||||
client = c;
|
||||
lobby = l;
|
||||
data = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the lobby value for the specific key
|
||||
/// </summary>
|
||||
/// <param name="k">The key to find</param>
|
||||
/// <returns>The value at key</returns>
|
||||
public string GetData( string k )
|
||||
{
|
||||
if ( data.ContainsKey( k ) )
|
||||
{
|
||||
return data[k];
|
||||
}
|
||||
|
||||
return "ERROR: key not found";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the data in the Lobby
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of all the key/value pairs in the data</returns>
|
||||
public Dictionary<string, string> GetAllData()
|
||||
{
|
||||
Dictionary<string, string> returnData = new Dictionary<string, string>();
|
||||
foreach ( KeyValuePair<string, string> item in data )
|
||||
{
|
||||
returnData.Add( item.Key, item.Value );
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the value for specified Key. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use.
|
||||
/// </summary>
|
||||
/// <param name="k">The key to set the value for</param>
|
||||
/// <param name="v">The value of the Key</param>
|
||||
/// <returns>True if data successfully set</returns>
|
||||
public bool SetData( string k, string v )
|
||||
{
|
||||
if ( data.ContainsKey( k ) )
|
||||
{
|
||||
if ( data[k] == v ) { return true; }
|
||||
if ( client.native.matchmaking.SetLobbyData( lobby, k, v ) )
|
||||
{
|
||||
data[k] = v;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( client.native.matchmaking.SetLobbyData( lobby, k, v ) )
|
||||
{
|
||||
data.Add( k, v );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the key from the LobbyData. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use.
|
||||
/// </summary>
|
||||
/// <param name="k">The key to remove</param>
|
||||
/// <returns>True if Key successfully removed</returns>
|
||||
public bool RemoveData( string k )
|
||||
{
|
||||
if ( data.ContainsKey( k ) )
|
||||
{
|
||||
if ( client.native.matchmaking.DeleteLobbyData( lobby, k ) )
|
||||
{
|
||||
data.Remove( k );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*not implemented
|
||||
|
||||
//set the game server of the lobby
|
||||
client.native.matchmaking.GetLobbyGameServer;
|
||||
client.native.matchmaking.SetLobbyGameServer;
|
||||
|
||||
//used with game server stuff
|
||||
SteamNative.LobbyGameCreated_t
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class Client : IDisposable
|
||||
{
|
||||
Lobby _lobby;
|
||||
|
||||
public Lobby Lobby
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _lobby == null )
|
||||
_lobby = new Steamworks.Lobby( this );
|
||||
return _lobby;
|
||||
}
|
||||
}
|
||||
}
|
||||
public partial class Lobby : IDisposable
|
||||
{
|
||||
//The type of lobby you are creating
|
||||
public enum Type : int
|
||||
{
|
||||
Private = SteamNative.LobbyType.Private,
|
||||
FriendsOnly = SteamNative.LobbyType.FriendsOnly,
|
||||
Public = SteamNative.LobbyType.Public,
|
||||
Invisible = SteamNative.LobbyType.Invisible,
|
||||
Error //happens if you try to get this when you aren't in a valid lobby
|
||||
}
|
||||
|
||||
internal Client client;
|
||||
|
||||
public Lobby( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
// For backwards compatibility
|
||||
OnLobbyJoinRequested = Join;
|
||||
|
||||
client.RegisterCallback<SteamNative.LobbyDataUpdate_t>( OnLobbyDataUpdatedAPI );
|
||||
client.RegisterCallback<SteamNative.LobbyChatMsg_t>( OnLobbyChatMessageRecievedAPI );
|
||||
client.RegisterCallback<SteamNative.LobbyChatUpdate_t>( OnLobbyStateUpdatedAPI );
|
||||
client.RegisterCallback<SteamNative.GameLobbyJoinRequested_t>( OnLobbyJoinRequestedAPI );
|
||||
client.RegisterCallback<SteamNative.LobbyInvite_t>( OnUserInvitedToLobbyAPI );
|
||||
client.RegisterCallback<SteamNative.PersonaStateChange_t>( OnLobbyMemberPersonaChangeAPI );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The CSteamID of the lobby we're currently in.
|
||||
/// </summary>
|
||||
public ulong CurrentLobby { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The LobbyData of the CurrentLobby. Note this is the global data for the lobby. Use SetMemberData to set specific member data.
|
||||
/// </summary>
|
||||
public LobbyData CurrentLobbyData { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this lobby is valid, ie, we've succesffuly created and/or joined a lobby.
|
||||
/// </summary>
|
||||
public bool IsValid => CurrentLobby != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Join a Lobby through its LobbyID. OnLobbyJoined is called with the result of the Join attempt.
|
||||
/// </summary>
|
||||
/// <param name="lobbyID">CSteamID of lobby to join</param>
|
||||
public void Join( ulong lobbyID )
|
||||
{
|
||||
Leave();
|
||||
client.native.matchmaking.JoinLobby( lobbyID, OnLobbyJoinedAPI );
|
||||
}
|
||||
|
||||
void OnLobbyJoinedAPI( LobbyEnter_t callback, bool error )
|
||||
{
|
||||
if ( error || (callback.EChatRoomEnterResponse != (uint)(SteamNative.ChatRoomEnterResponse.Success)) )
|
||||
{
|
||||
if ( OnLobbyJoined != null ) { OnLobbyJoined( false ); }
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentLobby = callback.SteamIDLobby;
|
||||
UpdateLobbyData();
|
||||
if ( OnLobbyJoined != null ) { OnLobbyJoined( true ); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a lobby has been attempted joined. Returns true if lobby was successfuly joined, false if not.
|
||||
/// </summary>
|
||||
public Action<bool> OnLobbyJoined;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a lobby and returns the created lobby. You auto join the created lobby. The lobby is stored in Client.Lobby.CurrentLobby if successful.
|
||||
/// </summary>
|
||||
/// <param name="lobbyType">The Lobby.Type of Lobby to be created</param>
|
||||
/// <param name="maxMembers">The maximum amount of people you want to be able to be in this lobby, including yourself</param>
|
||||
public void Create( Lobby.Type lobbyType, int maxMembers )
|
||||
{
|
||||
client.native.matchmaking.CreateLobby( (SteamNative.LobbyType)lobbyType, maxMembers, OnLobbyCreatedAPI );
|
||||
createdLobbyType = lobbyType;
|
||||
}
|
||||
|
||||
internal Type createdLobbyType;
|
||||
|
||||
internal void OnLobbyCreatedAPI( LobbyCreated_t callback, bool error )
|
||||
{
|
||||
//from SpaceWarClient.cpp 793
|
||||
if ( error || (callback.Result != Result.OK) )
|
||||
{
|
||||
if ( OnLobbyCreated != null ) { OnLobbyCreated( false ); }
|
||||
return;
|
||||
}
|
||||
|
||||
//set owner specific properties
|
||||
Owner = client.SteamId;
|
||||
CurrentLobby = callback.SteamIDLobby;
|
||||
CurrentLobbyData = new LobbyData( client, CurrentLobby );
|
||||
Name = client.Username + "'s Lobby";
|
||||
CurrentLobbyData.SetData( "appid", client.AppId.ToString() );
|
||||
LobbyType = createdLobbyType;
|
||||
CurrentLobbyData.SetData( "lobbytype", LobbyType.ToString() );
|
||||
Joinable = true;
|
||||
if ( OnLobbyCreated != null ) { OnLobbyCreated( true ); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for when lobby is created. Parameter resolves true when the Lobby was successfully created
|
||||
/// </summary>
|
||||
public Action<bool> OnLobbyCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Sets user data for the Lobby. Things like Character, Skin, Ready, etc. Can only set your own member data
|
||||
/// </summary>
|
||||
public void SetMemberData( string key, string value )
|
||||
{
|
||||
if ( CurrentLobby == 0 ) { return; }
|
||||
client.native.matchmaking.SetLobbyMemberData( CurrentLobby, key, value );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the per-user metadata from this lobby. Can get data from any user
|
||||
/// </summary>
|
||||
/// <param name="steamID">ulong SteamID of the user you want to get data from</param>
|
||||
/// <param name="key">String key of the type of data you want to get</param>
|
||||
/// <returns></returns>
|
||||
public string GetMemberData( ulong steamID, string key )
|
||||
{
|
||||
if ( CurrentLobby == 0 ) { return "ERROR: NOT IN ANY LOBBY"; }
|
||||
return client.native.matchmaking.GetLobbyMemberData( CurrentLobby, steamID, key );
|
||||
}
|
||||
|
||||
internal void OnLobbyDataUpdatedAPI( LobbyDataUpdate_t callback )
|
||||
{
|
||||
if ( callback.SteamIDLobby != CurrentLobby ) return;
|
||||
|
||||
if ( callback.SteamIDLobby == CurrentLobby ) //actual lobby data was updated by owner
|
||||
{
|
||||
UpdateLobbyData();
|
||||
}
|
||||
|
||||
if ( UserIsInCurrentLobby( callback.SteamIDMember ) ) //some member of this lobby updated their information
|
||||
{
|
||||
if ( OnLobbyMemberDataUpdated != null ) { OnLobbyMemberDataUpdated( callback.SteamIDMember ); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the LobbyData property to have the data for the current lobby, if any
|
||||
/// </summary>
|
||||
internal void UpdateLobbyData()
|
||||
{
|
||||
int dataCount = client.native.matchmaking.GetLobbyDataCount( CurrentLobby );
|
||||
CurrentLobbyData = new LobbyData( client, CurrentLobby );
|
||||
for ( int i = 0; i < dataCount; i++ )
|
||||
{
|
||||
if ( client.native.matchmaking.GetLobbyDataByIndex( CurrentLobby, i, out string key, out string value ) )
|
||||
{
|
||||
CurrentLobbyData.SetData( key, value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( OnLobbyDataUpdated != null ) { OnLobbyDataUpdated(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the lobby data itself has been updated. Called when someone has joined/left, Owner has updated data, etc.
|
||||
/// </summary>
|
||||
public Action OnLobbyDataUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a member of the lobby has updated either their personal Lobby metadata or someone's global steam state has changed (like a display name). Parameter is the user who changed.
|
||||
/// </summary>
|
||||
public Action<ulong> OnLobbyMemberDataUpdated;
|
||||
|
||||
|
||||
public Type LobbyType
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( !IsValid ) { return Type.Error; } //if we're currently in a valid server
|
||||
|
||||
//we know that we've set the lobby type via the lobbydata in the creation function
|
||||
//ps this is important because steam doesn't have an easy way to get lobby type (why idk)
|
||||
string lobbyType = CurrentLobbyData.GetData( "lobbytype" );
|
||||
switch ( lobbyType )
|
||||
{
|
||||
case "Private":
|
||||
return Type.Private;
|
||||
case "FriendsOnly":
|
||||
return Type.FriendsOnly;
|
||||
case "Invisible":
|
||||
return Type.Invisible;
|
||||
case "Public":
|
||||
return Type.Public;
|
||||
default:
|
||||
return Type.Error;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if ( !IsValid ) { return; }
|
||||
if ( client.native.matchmaking.SetLobbyType( CurrentLobby, (SteamNative.LobbyType)value ) )
|
||||
{
|
||||
CurrentLobbyData.SetData( "lobbytype", value.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] chatMessageData = new byte[1024 * 4];
|
||||
|
||||
private unsafe void OnLobbyChatMessageRecievedAPI( LobbyChatMsg_t callback )
|
||||
{
|
||||
//from Client.Networking
|
||||
if ( callback.SteamIDLobby != CurrentLobby )
|
||||
return;
|
||||
|
||||
SteamNative.CSteamID steamid = 1;
|
||||
ChatEntryType chatEntryType; // "If set then this will just always return k_EChatEntryTypeChatMsg. This can usually just be set to NULL."
|
||||
int readData = 0;
|
||||
fixed ( byte* p = chatMessageData )
|
||||
{
|
||||
readData = client.native.matchmaking.GetLobbyChatEntry( CurrentLobby, (int)callback.ChatID, out steamid, (IntPtr)p, chatMessageData.Length, out chatEntryType );
|
||||
}
|
||||
|
||||
|
||||
OnChatMessageRecieved?.Invoke( steamid, chatMessageData, readData );
|
||||
|
||||
if ( readData > 0 )
|
||||
{
|
||||
OnChatStringRecieved?.Invoke( steamid, Encoding.UTF8.GetString( chatMessageData, 0, readData ) );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback to get chat messages. Use Encoding.UTF8.GetString to retrive the message.
|
||||
/// </summary>
|
||||
public Action<ulong, byte[], int> OnChatMessageRecieved;
|
||||
|
||||
/// <summary>
|
||||
/// Like OnChatMessageRecieved but the data is converted to a string
|
||||
/// </summary>
|
||||
public Action<ulong, string> OnChatStringRecieved;
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts a chat message to the all the users in the lobby users in the lobby (including the local user) will receive a LobbyChatMsg_t callback.
|
||||
/// </summary>
|
||||
/// <returns>True if message successfully sent</returns>
|
||||
public unsafe bool SendChatMessage( string message )
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes( message );
|
||||
fixed ( byte* p = data )
|
||||
{
|
||||
// pvMsgBody can be binary or text data, up to 4k
|
||||
// if pvMsgBody is text, cubMsgBody should be strlen( text ) + 1, to include the null terminator
|
||||
return client.native.matchmaking.SendLobbyChatMsg( CurrentLobby, (IntPtr)p, data.Length );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enums to catch the state of a user when their state has changed
|
||||
/// </summary>
|
||||
public enum MemberStateChange
|
||||
{
|
||||
Entered = ChatMemberStateChange.Entered,
|
||||
Left = ChatMemberStateChange.Left,
|
||||
Disconnected = ChatMemberStateChange.Disconnected,
|
||||
Kicked = ChatMemberStateChange.Kicked,
|
||||
Banned = ChatMemberStateChange.Banned,
|
||||
}
|
||||
|
||||
internal void OnLobbyStateUpdatedAPI( LobbyChatUpdate_t callback )
|
||||
{
|
||||
if ( callback.SteamIDLobby != CurrentLobby )
|
||||
return;
|
||||
|
||||
MemberStateChange change = (MemberStateChange)callback.GfChatMemberStateChange;
|
||||
ulong initiator = callback.SteamIDMakingChange;
|
||||
ulong affected = callback.SteamIDUserChanged;
|
||||
|
||||
OnLobbyStateChanged?.Invoke( change, initiator, affected );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the state of the Lobby is somehow shifted. Usually when someone joins or leaves the lobby.
|
||||
/// The first ulong is the SteamID of the user that initiated the change.
|
||||
/// The second ulong is the person that was affected
|
||||
/// </summary>
|
||||
public Action<MemberStateChange, ulong, ulong> OnLobbyStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the lobby as a property for easy getting/setting. Note that this is setting LobbyData, which you cannot do unless you are the Owner of the lobby
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( !IsValid ) { return ""; }
|
||||
return CurrentLobbyData.GetData( "name" );
|
||||
}
|
||||
set
|
||||
{
|
||||
if ( !IsValid ) { return; }
|
||||
CurrentLobbyData.SetData( "name", value );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns true if we're the current owner
|
||||
/// </summary>
|
||||
public bool IsOwner
|
||||
{
|
||||
get
|
||||
{
|
||||
return Owner == client.SteamId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Owner of the current lobby. Returns 0 if you are not in a valid lobby.
|
||||
/// </summary>
|
||||
public ulong Owner
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( IsValid )
|
||||
{
|
||||
return client.native.matchmaking.GetLobbyOwner( CurrentLobby );
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
if ( Owner == value ) return;
|
||||
client.native.matchmaking.SetLobbyOwner( CurrentLobby, value );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the Lobby joinable by other people? Defaults to true;
|
||||
/// </summary>
|
||||
public bool Joinable
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( !IsValid ) { return false; }
|
||||
string joinable = CurrentLobbyData.GetData( "joinable" );
|
||||
switch ( joinable )
|
||||
{
|
||||
case "true":
|
||||
return true;
|
||||
case "false":
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if ( !IsValid ) { return; }
|
||||
if ( client.native.matchmaking.SetLobbyJoinable( CurrentLobby, value ) )
|
||||
{
|
||||
CurrentLobbyData.SetData( "joinable", value.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many people can be in the Lobby
|
||||
/// </summary>
|
||||
public int MaxMembers
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( !IsValid ) { return 0; } //0 is default, but value is inited when lobby is created.
|
||||
return client.native.matchmaking.GetLobbyMemberLimit( CurrentLobby );
|
||||
}
|
||||
set
|
||||
{
|
||||
if ( !IsValid ) { return; }
|
||||
client.native.matchmaking.SetLobbyMemberLimit( CurrentLobby, value );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many people are currently in the Lobby
|
||||
/// </summary>
|
||||
public int NumMembers
|
||||
{
|
||||
get { return client.native.matchmaking.GetNumLobbyMembers( CurrentLobby ); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leave the CurrentLobby.
|
||||
/// </summary>
|
||||
public void Leave()
|
||||
{
|
||||
if ( CurrentLobby != 0 )
|
||||
{
|
||||
client.native.matchmaking.LeaveLobby( CurrentLobby );
|
||||
}
|
||||
|
||||
CurrentLobby = 0;
|
||||
CurrentLobbyData = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an array of all the CSteamIDs in the CurrentLobby.
|
||||
/// Note that you must be in the Lobby you are trying to request the MemberIDs from.
|
||||
/// Returns an empty array if you aren't in a lobby.
|
||||
/// </summary>
|
||||
/// <returns>Array of member SteamIDs</returns>
|
||||
public ulong[] GetMemberIDs()
|
||||
{
|
||||
ulong[] memIDs = new ulong[NumMembers];
|
||||
for ( int i = 0; i < NumMembers; i++ )
|
||||
{
|
||||
memIDs[i] = client.native.matchmaking.GetLobbyMemberByIndex( CurrentLobby, i );
|
||||
}
|
||||
return memIDs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check to see if a user is in your CurrentLobby
|
||||
/// </summary>
|
||||
/// <param name="steamID">SteamID of the user to check for</param>
|
||||
/// <returns></returns>
|
||||
public bool UserIsInCurrentLobby( ulong steamID )
|
||||
{
|
||||
if ( CurrentLobby == 0 )
|
||||
return false;
|
||||
|
||||
ulong[] mems = GetMemberIDs();
|
||||
|
||||
for ( int i = 0; i < mems.Length; i++ )
|
||||
{
|
||||
if ( mems[i] == steamID )
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invites the specified user to the CurrentLobby the user is in.
|
||||
/// </summary>
|
||||
/// <param name="friendID">ulong ID of person to invite</param>
|
||||
public bool InviteUserToLobby( ulong friendID )
|
||||
{
|
||||
return client.native.matchmaking.InviteUserToLobby( CurrentLobby, friendID );
|
||||
}
|
||||
|
||||
internal void OnUserInvitedToLobbyAPI( LobbyInvite_t callback )
|
||||
{
|
||||
if ( callback.GameID != client.AppId ) return;
|
||||
if ( OnUserInvitedToLobby != null ) { OnUserInvitedToLobby( callback.SteamIDLobby, callback.SteamIDUser ); }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the steam overlay to invite friends to the CurrentLobby the user is in.
|
||||
/// </summary>
|
||||
public void OpenFriendInviteOverlay()
|
||||
{
|
||||
client.native.friends.ActivateGameOverlayInviteDialog(CurrentLobby);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a user invites the current user to a lobby. The first parameter is the lobby the user was invited to, the second is the CSteamID of the person who invited this user
|
||||
/// </summary>
|
||||
public Action<ulong, ulong> OnUserInvitedToLobby;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a user accepts an invitation to a lobby while the game is running. The parameter is a lobby id.
|
||||
/// </summary>
|
||||
public Action<ulong> OnLobbyJoinRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Joins a lobby if a request was made to join the lobby through the friends list or an invite
|
||||
/// </summary>
|
||||
internal void OnLobbyJoinRequestedAPI( GameLobbyJoinRequested_t callback )
|
||||
{
|
||||
if (OnLobbyJoinRequested != null) { OnLobbyJoinRequested(callback.SteamIDLobby); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure we send an update callback if a Lobby user updates their information
|
||||
/// </summary>
|
||||
internal void OnLobbyMemberPersonaChangeAPI( PersonaStateChange_t callback )
|
||||
{
|
||||
if ( !UserIsInCurrentLobby( callback.SteamID ) ) return;
|
||||
if ( OnLobbyMemberDataUpdated != null ) { OnLobbyMemberDataUpdated( callback.SteamID ); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the game server associated with the lobby.
|
||||
/// This can only be set by the owner of the lobby.
|
||||
/// Either the IP/Port or the Steam ID of the game server must be valid, depending on how you want the clients to be able to connect.
|
||||
/// </summary>
|
||||
public bool SetGameServer( System.Net.IPAddress ip, int port, ulong serverSteamId = 0 )
|
||||
{
|
||||
if ( !IsValid || !IsOwner ) return false;
|
||||
|
||||
var ipint = System.Net.IPAddress.NetworkToHostOrder( ip.Address );
|
||||
client.native.matchmaking.SetLobbyGameServer( CurrentLobby, (uint)ipint, (ushort)port, serverSteamId );
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of a game server set in a lobby.
|
||||
/// </summary>
|
||||
public System.Net.IPAddress GameServerIp
|
||||
{
|
||||
get
|
||||
{
|
||||
uint ip;
|
||||
ushort port;
|
||||
CSteamID steamid;
|
||||
|
||||
if ( !client.native.matchmaking.GetLobbyGameServer( CurrentLobby, out ip, out port, out steamid ) || ip == 0 )
|
||||
return null;
|
||||
|
||||
return new System.Net.IPAddress( System.Net.IPAddress.HostToNetworkOrder( ip ) );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of a game server set in a lobby.
|
||||
/// </summary>
|
||||
public int GameServerPort
|
||||
{
|
||||
get
|
||||
{
|
||||
uint ip;
|
||||
ushort port;
|
||||
CSteamID steamid;
|
||||
|
||||
if ( !client.native.matchmaking.GetLobbyGameServer( CurrentLobby, out ip, out port, out steamid ) )
|
||||
return 0;
|
||||
|
||||
return (int)port;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of a game server set in a lobby.
|
||||
/// </summary>
|
||||
public ulong GameServerSteamId
|
||||
{
|
||||
get
|
||||
{
|
||||
uint ip;
|
||||
ushort port;
|
||||
CSteamID steamid;
|
||||
|
||||
if ( !client.native.matchmaking.GetLobbyGameServer( CurrentLobby, out ip, out port, out steamid ) )
|
||||
return 0;
|
||||
|
||||
return steamid;
|
||||
}
|
||||
}
|
||||
|
||||
/*not implemented
|
||||
|
||||
//set the game server of the lobby
|
||||
client.native.matchmaking.GetLobbyGameServer;
|
||||
client.native.matchmaking.SetLobbyGameServer;
|
||||
|
||||
//used with game server stuff
|
||||
SteamNative.LobbyGameCreated_t
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class LobbyList
|
||||
{
|
||||
public class Lobby
|
||||
{
|
||||
private Dictionary<string, string> lobbyData;
|
||||
internal Client Client;
|
||||
public string Name { get; private set; }
|
||||
public ulong LobbyID { get; private set; }
|
||||
public ulong Owner { get; private set; }
|
||||
public int MemberLimit{ get; private set; }
|
||||
public int NumMembers{ get; private set; }
|
||||
public string LobbyType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the lobby value for the specific key
|
||||
/// </summary>
|
||||
/// <param name="k">The key to find</param>
|
||||
/// <returns>The value at key</returns>
|
||||
public string GetData(string k)
|
||||
{
|
||||
if (lobbyData.TryGetValue(k, out var v))
|
||||
return v;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the data in the Lobby
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of all the key/value pairs in the data</returns>
|
||||
public Dictionary<string, string> GetAllData()
|
||||
{
|
||||
var returnData = new Dictionary<string, string>();
|
||||
|
||||
foreach ( var item in lobbyData)
|
||||
{
|
||||
returnData.Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
internal static Lobby FromSteam(Client client, ulong lobby)
|
||||
{
|
||||
var lobbyData = new Dictionary<string, string>();
|
||||
int dataCount = client.native.matchmaking.GetLobbyDataCount(lobby);
|
||||
|
||||
for (int i = 0; i < dataCount; i++)
|
||||
{
|
||||
if (client.native.matchmaking.GetLobbyDataByIndex(lobby, i, out var datakey, out var datavalue))
|
||||
{
|
||||
lobbyData.Add(datakey, datavalue);
|
||||
}
|
||||
}
|
||||
|
||||
return new Lobby()
|
||||
{
|
||||
Client = client,
|
||||
LobbyID = lobby,
|
||||
Name = client.native.matchmaking.GetLobbyData(lobby, "name"),
|
||||
LobbyType = client.native.matchmaking.GetLobbyData(lobby, "lobbytype"),
|
||||
MemberLimit = client.native.matchmaking.GetLobbyMemberLimit(lobby),
|
||||
Owner = client.native.matchmaking.GetLobbyOwner(lobby),
|
||||
NumMembers = client.native.matchmaking.GetNumLobbyMembers(lobby),
|
||||
lobbyData = lobbyData
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class LobbyList : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
//The list of retrieved lobbies
|
||||
public List<Lobby> Lobbies { get; private set; }
|
||||
|
||||
//True when all the possible lobbies have had their data updated
|
||||
//if the number of lobbies is now equal to the initial request number, we've found all lobbies
|
||||
public bool Finished { get; private set; }
|
||||
|
||||
//The number of possible lobbies we can get data from
|
||||
internal List<ulong> requests;
|
||||
|
||||
internal LobbyList(Client client)
|
||||
{
|
||||
this.client = client;
|
||||
Lobbies = new List<Lobby>();
|
||||
requests = new List<ulong>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the List of Lobbies. If no filter is passed in, a default one is created that filters based on AppId ("appid").
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
public void Refresh ( Filter filter = null)
|
||||
{
|
||||
//init out values
|
||||
Lobbies.Clear();
|
||||
requests.Clear();
|
||||
Finished = false;
|
||||
|
||||
if (filter == null)
|
||||
{
|
||||
filter = new Filter();
|
||||
filter.StringFilters.Add("appid", client.AppId.ToString());
|
||||
client.native.matchmaking.RequestLobbyList(OnLobbyList);
|
||||
return;
|
||||
}
|
||||
|
||||
client.native.matchmaking.AddRequestLobbyListDistanceFilter((SteamNative.LobbyDistanceFilter)filter.DistanceFilter);
|
||||
|
||||
if (filter.SlotsAvailable != null)
|
||||
{
|
||||
client.native.matchmaking.AddRequestLobbyListFilterSlotsAvailable((int)filter.SlotsAvailable);
|
||||
}
|
||||
|
||||
if (filter.MaxResults != null)
|
||||
{
|
||||
client.native.matchmaking.AddRequestLobbyListResultCountFilter((int)filter.MaxResults);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> fil in filter.StringFilters)
|
||||
{
|
||||
client.native.matchmaking.AddRequestLobbyListStringFilter(fil.Key, fil.Value, SteamNative.LobbyComparison.Equal);
|
||||
}
|
||||
foreach (KeyValuePair<string, int> fil in filter.NearFilters)
|
||||
{
|
||||
client.native.matchmaking.AddRequestLobbyListNearValueFilter(fil.Key, fil.Value);
|
||||
}
|
||||
//foreach (KeyValuePair<string, KeyValuePair<Filter.Comparison, int>> fil in filter.NumericalFilters)
|
||||
//{
|
||||
// client.native.matchmaking.AddRequestLobbyListNumericalFilter(fil.Key, fil.Value.Value, (SteamNative.LobbyComparison)fil.Value.Key);
|
||||
//}
|
||||
|
||||
|
||||
// this will never return lobbies that are full (via the actual api)
|
||||
client.native.matchmaking.RequestLobbyList(OnLobbyList);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void OnLobbyList(LobbyMatchList_t callback, bool error)
|
||||
{
|
||||
if (error) return;
|
||||
|
||||
//how many lobbies matched
|
||||
uint lobbiesMatching = callback.LobbiesMatching;
|
||||
|
||||
// lobbies are returned in order of closeness to the user, so add them to the list in that order
|
||||
for (int i = 0; i < lobbiesMatching; i++)
|
||||
{
|
||||
//add the lobby to the list of requests
|
||||
ulong lobby = client.native.matchmaking.GetLobbyByIndex(i);
|
||||
requests.Add(lobby);
|
||||
|
||||
//cast to a LobbyList.Lobby
|
||||
Lobby newLobby = Lobby.FromSteam(client, lobby);
|
||||
if (newLobby.Name != "")
|
||||
{
|
||||
//if the lobby is valid add it to the valid return lobbies
|
||||
Lobbies.Add(newLobby);
|
||||
checkFinished();
|
||||
}
|
||||
else
|
||||
{
|
||||
//else we need to get the info for the missing lobby
|
||||
client.native.matchmaking.RequestLobbyData(lobby);
|
||||
client.RegisterCallback<SteamNative.LobbyDataUpdate_t>( OnLobbyDataUpdated );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkFinished();
|
||||
|
||||
if (OnLobbiesUpdated != null) { OnLobbiesUpdated(); }
|
||||
}
|
||||
|
||||
void checkFinished()
|
||||
{
|
||||
if (Lobbies.Count == requests.Count)
|
||||
{
|
||||
Finished = true;
|
||||
return;
|
||||
}
|
||||
Finished = false;
|
||||
}
|
||||
|
||||
void OnLobbyDataUpdated(LobbyDataUpdate_t callback)
|
||||
{
|
||||
if (callback.Success == 1) //1 if success, 0 if failure
|
||||
{
|
||||
//find the lobby that has been updated
|
||||
Lobby lobby = Lobbies.Find(x => x.LobbyID == callback.SteamIDLobby);
|
||||
|
||||
//if this lobby isn't yet in the list of lobbies, we know that we should add it
|
||||
if (lobby == null)
|
||||
{
|
||||
Lobbies.Add(lobby);
|
||||
checkFinished();
|
||||
}
|
||||
|
||||
//otherwise lobby data in general was updated and you should listen to see what changed
|
||||
if (OnLobbiesUpdated != null) { OnLobbiesUpdated(); }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public Action OnLobbiesUpdated;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
public class Filter
|
||||
{
|
||||
// Filters that match actual metadata keys exactly
|
||||
public Dictionary<string, string> StringFilters = new Dictionary<string, string>();
|
||||
// Filters that are of string key and int value for that key to be close to
|
||||
public Dictionary<string, int> NearFilters = new Dictionary<string, int>();
|
||||
//Filters that are of string key and int value, with a comparison filter to say how we should relate to the value
|
||||
//public Dictionary<string, KeyValuePair<Comparison, int>> NumericalFilters = new Dictionary<string, KeyValuePair<Comparison, int>>();
|
||||
public Distance DistanceFilter = Distance.Worldwide;
|
||||
public int? SlotsAvailable { get; set; }
|
||||
public int? MaxResults { get; set; }
|
||||
|
||||
public enum Distance : int
|
||||
{
|
||||
Close = SteamNative.LobbyDistanceFilter.Close,
|
||||
Default = SteamNative.LobbyDistanceFilter.Default,
|
||||
Far = SteamNative.LobbyDistanceFilter.Far,
|
||||
Worldwide = SteamNative.LobbyDistanceFilter.Worldwide
|
||||
}
|
||||
|
||||
public enum Comparison : int
|
||||
{
|
||||
EqualToOrLessThan = SteamNative.LobbyComparison.EqualToOrLessThan,
|
||||
LessThan = SteamNative.LobbyComparison.LessThan,
|
||||
Equal = SteamNative.LobbyComparison.Equal,
|
||||
GreaterThan = SteamNative.LobbyComparison.GreaterThan,
|
||||
EqualToOrGreaterThan = SteamNative.LobbyComparison.EqualToOrGreaterThan,
|
||||
NotEqual = SteamNative.LobbyComparison.NotEqual
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class MicroTransactions : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public delegate void AuthorizationResponse( bool authorized, int appId, ulong orderId );
|
||||
|
||||
/// <summary>
|
||||
/// Called on the MicroTxnAuthorizationResponse_t event
|
||||
/// </summary>
|
||||
public event AuthorizationResponse OnAuthorizationResponse;
|
||||
|
||||
internal MicroTransactions( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
client.RegisterCallback<SteamNative.MicroTxnAuthorizationResponse_t>( onMicroTxnAuthorizationResponse );
|
||||
}
|
||||
|
||||
private void onMicroTxnAuthorizationResponse( MicroTxnAuthorizationResponse_t arg1 )
|
||||
{
|
||||
if ( OnAuthorizationResponse != null )
|
||||
{
|
||||
OnAuthorizationResponse( arg1.Authorized == 1, (int) arg1.AppID, arg1.OrderID );
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class Overlay
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get { return client.native.utils.IsOverlayEnabled(); }
|
||||
}
|
||||
|
||||
public bool IsOpen { get; private set; }
|
||||
|
||||
internal Overlay( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
c.RegisterCallback<GameOverlayActivated_t>( OverlayStateChange );
|
||||
}
|
||||
|
||||
private void OverlayStateChange( GameOverlayActivated_t activation )
|
||||
{
|
||||
IsOpen = activation.Active == 1;
|
||||
}
|
||||
|
||||
public void OpenUserPage( string name, ulong steamid ) { client.native.friends.ActivateGameOverlayToUser( name, steamid ); }
|
||||
|
||||
public void OpenProfile( ulong steamid ) { OpenUserPage( "steamid", steamid ); }
|
||||
public void OpenChat( ulong steamid ){ OpenUserPage( "chat", steamid ); }
|
||||
public void OpenTrade( ulong steamid ) { OpenUserPage( "jointrade", steamid ); }
|
||||
public void OpenStats( ulong steamid ) { OpenUserPage( "stats", steamid ); }
|
||||
public void OpenAchievements( ulong steamid ) { OpenUserPage( "achievements", steamid ); }
|
||||
public void AddFriend( ulong steamid ) { OpenUserPage( "friendadd", steamid ); }
|
||||
public void RemoveFriend( ulong steamid ) { OpenUserPage( "friendremove", steamid ); }
|
||||
public void AcceptFriendRequest( ulong steamid ) { OpenUserPage( "friendrequestaccept", steamid ); }
|
||||
public void IgnoreFriendRequest( ulong steamid ) { OpenUserPage( "friendrequestignore", steamid ); }
|
||||
|
||||
public void OpenUrl( string url ) { client.native.friends.ActivateGameOverlayToWebPage( url ); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Facepunch.Steamworks.Callbacks;
|
||||
using SteamNative;
|
||||
using Result = SteamNative.Result;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a file stored in a user's Steam Cloud.
|
||||
/// </summary>
|
||||
public class RemoteFile
|
||||
{
|
||||
internal readonly RemoteStorage remoteStorage;
|
||||
|
||||
private readonly bool _isUgc;
|
||||
private string _fileName;
|
||||
private int _sizeInBytes = -1;
|
||||
private long _timestamp = 0;
|
||||
private UGCHandle_t _handle;
|
||||
private ulong _ownerId;
|
||||
|
||||
private bool _isDownloading;
|
||||
private byte[] _downloadedData;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the file exists.
|
||||
/// </summary>
|
||||
public bool Exists { get; internal set; }
|
||||
|
||||
public bool IsDownloading { get { return _isUgc && _isDownloading && _downloadedData == null; } }
|
||||
|
||||
public bool IsDownloaded { get { return !_isUgc || _downloadedData != null; } }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the file is available for other users to download.
|
||||
/// </summary>
|
||||
public bool IsShared { get { return _handle.Value != 0; } }
|
||||
|
||||
internal UGCHandle_t UGCHandle { get { return _handle; } }
|
||||
|
||||
public ulong SharingId { get { return UGCHandle.Value; } }
|
||||
|
||||
/// <summary>
|
||||
/// Name and path of the file.
|
||||
/// </summary>
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _fileName != null ) return _fileName;
|
||||
GetUGCDetails();
|
||||
return _fileName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Steam ID of the file's owner.
|
||||
/// </summary>
|
||||
public ulong OwnerId
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _ownerId != 0 ) return _ownerId;
|
||||
GetUGCDetails();
|
||||
return _ownerId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total size of the file in bytes.
|
||||
/// </summary>
|
||||
public int SizeInBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _sizeInBytes != -1 ) return _sizeInBytes;
|
||||
if ( _isUgc ) throw new NotImplementedException();
|
||||
_sizeInBytes = remoteStorage.native.GetFileSize( FileName );
|
||||
return _sizeInBytes;
|
||||
}
|
||||
internal set { _sizeInBytes = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Date modified timestamp in epoch format.
|
||||
/// </summary>
|
||||
public long FileTimestamp
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _timestamp != 0 ) return _timestamp;
|
||||
if (_isUgc) throw new NotImplementedException();
|
||||
_timestamp = remoteStorage.native.GetFileTimestamp(FileName);
|
||||
return _timestamp;
|
||||
}
|
||||
internal set { _timestamp = value; }
|
||||
}
|
||||
|
||||
internal RemoteFile( RemoteStorage r, UGCHandle_t handle )
|
||||
{
|
||||
Exists = true;
|
||||
|
||||
remoteStorage = r;
|
||||
|
||||
_isUgc = true;
|
||||
_handle = handle;
|
||||
}
|
||||
|
||||
internal RemoteFile( RemoteStorage r, string name, ulong ownerId, int sizeInBytes = -1, long timestamp = 0 )
|
||||
{
|
||||
remoteStorage = r;
|
||||
|
||||
_isUgc = false;
|
||||
_fileName = name;
|
||||
_ownerId = ownerId;
|
||||
_sizeInBytes = sizeInBytes;
|
||||
_timestamp = timestamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RemoteFileWriteStream"/> used to write to this file.
|
||||
/// </summary>
|
||||
public RemoteFileWriteStream OpenWrite()
|
||||
{
|
||||
if (_isUgc) throw new InvalidOperationException("Cannot write to a shared file.");
|
||||
|
||||
return new RemoteFileWriteStream( remoteStorage, this );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a byte array to this file, replacing any existing contents.
|
||||
/// </summary>
|
||||
public void WriteAllBytes( byte[] buffer )
|
||||
{
|
||||
using ( var stream = OpenWrite() )
|
||||
{
|
||||
stream.Write( buffer, 0, buffer.Length );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string to this file, replacing any existing contents.
|
||||
/// </summary>
|
||||
public void WriteAllText( string text, Encoding encoding = null )
|
||||
{
|
||||
if ( encoding == null ) encoding = Encoding.UTF8;
|
||||
WriteAllBytes( encoding.GetBytes( text ) );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by <see cref="RemoteFile.Download"/> when a file download is complete.
|
||||
/// </summary>
|
||||
public delegate void DownloadCallback();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of bytes downloaded and the total number of bytes expected while
|
||||
/// this file is downloading.
|
||||
/// </summary>
|
||||
/// <returns>True if the file is downloading</returns>
|
||||
public bool GetDownloadProgress( out int bytesDownloaded, out int bytesExpected )
|
||||
{
|
||||
return remoteStorage.native.GetUGCDownloadProgress( _handle, out bytesDownloaded, out bytesExpected );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to start downloading a shared file.
|
||||
/// </summary>
|
||||
/// <returns>True if the download has successfully started</returns>
|
||||
public bool Download( DownloadCallback onSuccess = null, FailureCallback onFailure = null )
|
||||
{
|
||||
if ( !_isUgc ) return false;
|
||||
if ( _isDownloading ) return false;
|
||||
if ( IsDownloaded ) return false;
|
||||
|
||||
_isDownloading = true;
|
||||
|
||||
remoteStorage.native.UGCDownload( _handle, 1000, ( result, error ) =>
|
||||
{
|
||||
_isDownloading = false;
|
||||
|
||||
if ( error || result.Result != Result.OK )
|
||||
{
|
||||
onFailure?.Invoke( result.Result == 0 ? Callbacks.Result.IOFailure : (Callbacks.Result) result.Result );
|
||||
return;
|
||||
}
|
||||
|
||||
_ownerId = result.SteamIDOwner;
|
||||
_sizeInBytes = result.SizeInBytes;
|
||||
_fileName = result.PchFileName;
|
||||
|
||||
unsafe
|
||||
{
|
||||
_downloadedData = new byte[_sizeInBytes];
|
||||
fixed ( byte* bufferPtr = _downloadedData )
|
||||
{
|
||||
remoteStorage.native.UGCRead( _handle, (IntPtr) bufferPtr, _sizeInBytes, 0, UGCReadAction.ontinueReading );
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess?.Invoke();
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a stream used to read from this file.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Stream OpenRead()
|
||||
{
|
||||
return new MemoryStream( ReadAllBytes(), false );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the entire contents of the file as a byte array.
|
||||
/// </summary>
|
||||
public unsafe byte[] ReadAllBytes()
|
||||
{
|
||||
if ( _isUgc )
|
||||
{
|
||||
if ( !IsDownloaded ) throw new Exception( "Cannot read a file that hasn't been downloaded." );
|
||||
return _downloadedData;
|
||||
}
|
||||
|
||||
var size = SizeInBytes;
|
||||
var buffer = new byte[size];
|
||||
|
||||
fixed ( byte* bufferPtr = buffer )
|
||||
{
|
||||
remoteStorage.native.FileRead( FileName, (IntPtr) bufferPtr, size );
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the entire contents of the file as a string.
|
||||
/// </summary>
|
||||
public string ReadAllText( Encoding encoding = null )
|
||||
{
|
||||
if ( encoding == null ) encoding = Encoding.UTF8;
|
||||
return encoding.GetString( ReadAllBytes() );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by <see cref="RemoteFile.Share"/> when file sharing is complete.
|
||||
/// </summary>
|
||||
public delegate void ShareCallback();
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to publish this file for other users to download.
|
||||
/// </summary>
|
||||
/// <returns>True if we have started attempting to share</returns>
|
||||
public bool Share( ShareCallback onSuccess = null, FailureCallback onFailure = null )
|
||||
{
|
||||
if ( _isUgc ) return false;
|
||||
|
||||
// Already shared
|
||||
if ( _handle.Value != 0 ) return false;
|
||||
|
||||
remoteStorage.native.FileShare( FileName, ( result, error ) =>
|
||||
{
|
||||
if ( !error && result.Result == Result.OK )
|
||||
{
|
||||
_handle.Value = result.File;
|
||||
onSuccess?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
onFailure?.Invoke( result.Result == 0 ? Callbacks.Result.IOFailure : (Callbacks.Result) result.Result );
|
||||
}
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete this file from remote storage.
|
||||
/// </summary>
|
||||
/// <returns>True if the file could be deleted</returns>
|
||||
public bool Delete()
|
||||
{
|
||||
if ( !Exists ) return false;
|
||||
if ( _isUgc ) return false;
|
||||
if ( !remoteStorage.native.FileDelete( FileName ) ) return false;
|
||||
|
||||
Exists = false;
|
||||
remoteStorage.InvalidateFiles();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove this file from remote storage, while keeping a local copy.
|
||||
/// Writing to this file again will re-add it to the cloud.
|
||||
/// </summary>
|
||||
/// <returns>True if the file was forgotten</returns>
|
||||
public bool Forget()
|
||||
{
|
||||
if ( !Exists ) return false;
|
||||
if ( _isUgc ) return false;
|
||||
|
||||
return remoteStorage.native.FileForget( FileName );
|
||||
}
|
||||
|
||||
private void GetUGCDetails()
|
||||
{
|
||||
if ( !_isUgc ) throw new InvalidOperationException();
|
||||
|
||||
var appId = new AppId_t { Value = remoteStorage.native.steamworks.AppId };
|
||||
|
||||
CSteamID ownerId;
|
||||
remoteStorage.native.GetUGCDetails( _handle, ref appId, out _fileName, out ownerId );
|
||||
|
||||
_ownerId = ownerId.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
/// <summary>
|
||||
/// Stream used to write to a <see cref="RemoteFile"/>.
|
||||
/// </summary>
|
||||
public class RemoteFileWriteStream : Stream
|
||||
{
|
||||
internal readonly RemoteStorage remoteStorage;
|
||||
|
||||
private readonly UGCFileWriteStreamHandle_t _handle;
|
||||
private readonly RemoteFile _file;
|
||||
|
||||
private int _written;
|
||||
private bool _closed;
|
||||
|
||||
internal RemoteFileWriteStream( RemoteStorage r, RemoteFile file )
|
||||
{
|
||||
remoteStorage = r;
|
||||
|
||||
_handle = remoteStorage.native.FileWriteStreamOpen( file.FileName );
|
||||
_file = file;
|
||||
}
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override int Read( byte[] buffer, int offset, int count )
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override long Seek( long offset, SeekOrigin origin )
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void SetLength( long value )
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override unsafe void Write( byte[] buffer, int offset, int count )
|
||||
{
|
||||
if ( _closed ) throw new ObjectDisposedException( ToString() );
|
||||
|
||||
fixed ( byte* bufferPtr = buffer )
|
||||
{
|
||||
if ( remoteStorage.native.FileWriteStreamWriteChunk( _handle, (IntPtr)(bufferPtr + offset), count ) )
|
||||
{
|
||||
_written += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
public override long Length => _written;
|
||||
public override long Position { get { return _written; } set { throw new NotImplementedException(); } }
|
||||
|
||||
/// <summary>
|
||||
/// Close the stream without saving the file to remote storage.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
if ( _closed ) return;
|
||||
|
||||
_closed = true;
|
||||
remoteStorage.native.FileWriteStreamCancel( _handle );
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if ( _closed ) return;
|
||||
|
||||
_closed = true;
|
||||
remoteStorage.native.FileWriteStreamClose( _handle );
|
||||
|
||||
_file.remoteStorage.OnWrittenNewFile( _file );
|
||||
}
|
||||
|
||||
protected override void Dispose( bool disposing )
|
||||
{
|
||||
if ( disposing ) Close();
|
||||
base.Dispose( disposing );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles Steam Cloud related actions.
|
||||
/// </summary>
|
||||
public class RemoteStorage : IDisposable
|
||||
{
|
||||
private static string NormalizePath( string path )
|
||||
{
|
||||
// TODO: DUMB HACK ALERT
|
||||
|
||||
return SteamNative.Platform.IsWindows
|
||||
? new FileInfo( $"x:/{path}" ).FullName.Substring( 3 )
|
||||
: new FileInfo( $"/x/{path}" ).FullName.Substring( 3 );
|
||||
}
|
||||
|
||||
internal Client client;
|
||||
internal SteamNative.SteamRemoteStorage native;
|
||||
|
||||
private bool _filesInvalid = true;
|
||||
private readonly List<RemoteFile> _files = new List<RemoteFile>();
|
||||
|
||||
internal RemoteStorage( Client c )
|
||||
{
|
||||
client = c;
|
||||
native = client.native.remoteStorage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if Steam Cloud is currently enabled by the current user.
|
||||
/// </summary>
|
||||
public bool IsCloudEnabledForAccount
|
||||
{
|
||||
get { return native.IsCloudEnabledForAccount(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if Steam Cloud is currently enabled for this app by the current user.
|
||||
/// </summary>
|
||||
public bool IsCloudEnabledForApp
|
||||
{
|
||||
get { return native.IsCloudEnabledForApp(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of files in the current user's remote storage for the current game.
|
||||
/// </summary>
|
||||
public int FileCount
|
||||
{
|
||||
get { return native.GetFileCount(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files in the current user's remote storage for the current game.
|
||||
/// </summary>
|
||||
public IEnumerable<RemoteFile> Files
|
||||
{
|
||||
get
|
||||
{
|
||||
UpdateFiles();
|
||||
return _files;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RemoteFile"/> with the given <paramref name="path"/>.
|
||||
/// If a file exists at that path it will be overwritten.
|
||||
/// </summary>
|
||||
public RemoteFile CreateFile( string path )
|
||||
{
|
||||
path = NormalizePath( path );
|
||||
|
||||
InvalidateFiles();
|
||||
var existing = Files.FirstOrDefault( x => x.FileName == path );
|
||||
return existing ?? new RemoteFile( this, path, client.SteamId, 0 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the file if it exists, else returns null;
|
||||
/// </summary>
|
||||
public RemoteFile OpenFile( string path )
|
||||
{
|
||||
path = NormalizePath( path );
|
||||
|
||||
InvalidateFiles();
|
||||
var existing = Files.FirstOrDefault( x => x.FileName == path );
|
||||
return existing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a previously shared <see cref="RemoteFile"/>
|
||||
/// with the given <paramref name="sharingId"/>.
|
||||
/// </summary>
|
||||
public RemoteFile OpenSharedFile( ulong sharingId )
|
||||
{
|
||||
return new RemoteFile( this, sharingId );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write all text to the file at the specified path. This
|
||||
/// overwrites the contents - it does not append.
|
||||
/// </summary>
|
||||
public bool WriteString( string path, string text, Encoding encoding = null )
|
||||
{
|
||||
var file = CreateFile( path );
|
||||
file.WriteAllText( text, encoding );
|
||||
return file.Exists;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write all data to the file at the specified path. This
|
||||
/// overwrites the contents - it does not append.
|
||||
/// </summary>
|
||||
public bool WriteBytes( string path, byte[] data )
|
||||
{
|
||||
var file = CreateFile( path );
|
||||
file.WriteAllBytes( data );
|
||||
return file.Exists;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the entire contents of the file as a string.
|
||||
/// Returns null if the file isn't found.
|
||||
/// </summary>
|
||||
public string ReadString( string path, Encoding encoding = null )
|
||||
{
|
||||
var file = OpenFile( path );
|
||||
if ( file == null ) return null;
|
||||
return file.ReadAllText( encoding );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the entire contents of the file as raw data.
|
||||
/// Returns null if the file isn't found.
|
||||
/// </summary>
|
||||
public byte[] ReadBytes( string path )
|
||||
{
|
||||
var file = OpenFile( path );
|
||||
if ( file == null ) return null;
|
||||
return file.ReadAllBytes();
|
||||
}
|
||||
|
||||
internal void OnWrittenNewFile( RemoteFile file )
|
||||
{
|
||||
if ( _files.Any( x => x.FileName == file.FileName ) ) return;
|
||||
|
||||
_files.Add( file );
|
||||
file.Exists = true;
|
||||
|
||||
InvalidateFiles();
|
||||
}
|
||||
|
||||
internal void InvalidateFiles()
|
||||
{
|
||||
_filesInvalid = true;
|
||||
}
|
||||
|
||||
private void UpdateFiles()
|
||||
{
|
||||
if ( !_filesInvalid ) return;
|
||||
_filesInvalid = false;
|
||||
|
||||
foreach ( var file in _files )
|
||||
{
|
||||
file.Exists = false;
|
||||
}
|
||||
|
||||
var count = FileCount;
|
||||
for ( var i = 0; i < count; ++i )
|
||||
{
|
||||
int size;
|
||||
var name = NormalizePath( native.GetFileNameAndSize( i, out size ) );
|
||||
var timestamp = native.GetFileTimestamp(name);
|
||||
|
||||
var existing = _files.FirstOrDefault( x => x.FileName == name );
|
||||
if ( existing == null )
|
||||
{
|
||||
existing = new RemoteFile( this, name, client.SteamId, size, timestamp );
|
||||
_files.Add( existing );
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.SizeInBytes = size;
|
||||
existing.FileTimestamp = timestamp;
|
||||
}
|
||||
|
||||
existing.Exists = true;
|
||||
}
|
||||
|
||||
for ( var i = _files.Count - 1; i >= 0; --i )
|
||||
{
|
||||
if ( !_files[i].Exists ) _files.RemoveAt( i );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a file exists in remote storage at the given <paramref name="path"/>.
|
||||
/// </summary>
|
||||
public bool FileExists( string path )
|
||||
{
|
||||
return native.FileExists( path );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
native = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes used out of the user's total quota
|
||||
/// </summary>
|
||||
public ulong QuotaUsed
|
||||
{
|
||||
get
|
||||
{
|
||||
ulong totalBytes = 0;
|
||||
ulong availableBytes = 0;
|
||||
|
||||
if ( !native.GetQuota( out totalBytes, out availableBytes ) )
|
||||
return 0;
|
||||
|
||||
return totalBytes - availableBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total quota size in bytes
|
||||
/// </summary>
|
||||
public ulong QuotaTotal
|
||||
{
|
||||
get
|
||||
{
|
||||
ulong totalBytes = 0;
|
||||
ulong availableBytes = 0;
|
||||
|
||||
if ( !native.GetQuota( out totalBytes, out availableBytes ) )
|
||||
return 0;
|
||||
|
||||
return totalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes remaining out of the user's total quota
|
||||
/// </summary>
|
||||
public ulong QuotaRemaining
|
||||
{
|
||||
get
|
||||
{
|
||||
ulong totalBytes = 0;
|
||||
ulong availableBytes = 0;
|
||||
|
||||
if ( !native.GetQuota( out totalBytes, out availableBytes ) )
|
||||
return 0;
|
||||
|
||||
return availableBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class Client : IDisposable
|
||||
{
|
||||
Screenshots _screenshots;
|
||||
|
||||
public Screenshots Screenshots
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _screenshots == null )
|
||||
_screenshots = new Screenshots( this );
|
||||
|
||||
return _screenshots;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Screenshots
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
internal Screenshots( Client c )
|
||||
{
|
||||
client = c;
|
||||
}
|
||||
|
||||
public void Trigger()
|
||||
{
|
||||
client.native.screenshots.TriggerScreenshot();
|
||||
}
|
||||
|
||||
public unsafe void Write( byte[] rgbData, int width, int height )
|
||||
{
|
||||
if ( rgbData == null )
|
||||
{
|
||||
throw new ArgumentNullException( nameof(rgbData) );
|
||||
}
|
||||
|
||||
if ( width < 1 )
|
||||
{
|
||||
throw new ArgumentOutOfRangeException( nameof(width), width,
|
||||
$"Expected {nameof(width)} to be at least 1." );
|
||||
}
|
||||
|
||||
if ( height < 1 )
|
||||
{
|
||||
throw new ArgumentOutOfRangeException( nameof(height), height,
|
||||
$"Expected {nameof(height)} to be at least 1." );
|
||||
}
|
||||
|
||||
var size = width * height * 3;
|
||||
if ( rgbData.Length < size )
|
||||
{
|
||||
throw new ArgumentException( nameof(rgbData),
|
||||
$"Expected {nameof(rgbData)} to contain at least {size} elements (actual size: {rgbData.Length})." );
|
||||
}
|
||||
|
||||
fixed ( byte* ptr = rgbData )
|
||||
{
|
||||
client.native.screenshots.WriteScreenshot( (IntPtr) ptr, (uint) rgbData.Length, width, height );
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void AddScreenshotToLibrary( string filename, string thumbnailFilename, int width, int height)
|
||||
{
|
||||
client.native.screenshots.AddScreenshotToLibrary(filename, thumbnailFilename, width, height);
|
||||
}
|
||||
|
||||
public unsafe void AddScreenshotToLibrary( string filename, int width, int height)
|
||||
{
|
||||
client.native.screenshots.AddScreenshotToLibrary(filename, null, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class ServerList
|
||||
{
|
||||
public class Request : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
internal List<SubRequest> Requests = new List<SubRequest>();
|
||||
|
||||
internal class SubRequest
|
||||
{
|
||||
internal IntPtr Request;
|
||||
internal int Pointer = 0;
|
||||
internal List<int> WatchList = new List<int>();
|
||||
internal System.Diagnostics.Stopwatch Timer = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
internal bool Update( SteamNative.SteamMatchmakingServers servers, Action<SteamNative.gameserveritem_t> OnServer, Action OnUpdate )
|
||||
{
|
||||
if ( Request == IntPtr.Zero )
|
||||
return true;
|
||||
|
||||
if ( Timer.Elapsed.TotalSeconds < 0.5f )
|
||||
return false;
|
||||
|
||||
Timer.Reset();
|
||||
Timer.Start();
|
||||
|
||||
bool changes = false;
|
||||
|
||||
//
|
||||
// Add any servers we're not watching to our watch list
|
||||
//
|
||||
var count = servers.GetServerCount( Request );
|
||||
if ( count != Pointer )
|
||||
{
|
||||
for ( int i = Pointer; i < count; i++ )
|
||||
{
|
||||
WatchList.Add( i );
|
||||
}
|
||||
}
|
||||
Pointer = count;
|
||||
|
||||
//
|
||||
// Remove any servers that respond successfully
|
||||
//
|
||||
WatchList.RemoveAll( x =>
|
||||
{
|
||||
var info = servers.GetServerDetails( Request, x );
|
||||
if ( info.HadSuccessfulResponse )
|
||||
{
|
||||
OnServer( info );
|
||||
changes = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} );
|
||||
|
||||
//
|
||||
// If we've finished refreshing
|
||||
//
|
||||
if ( servers.IsRefreshing( Request ) == false )
|
||||
{
|
||||
//
|
||||
// Put any other servers on the 'no response' list
|
||||
//
|
||||
WatchList.RemoveAll( x =>
|
||||
{
|
||||
var info = servers.GetServerDetails( Request, x );
|
||||
OnServer( info );
|
||||
return true;
|
||||
} );
|
||||
|
||||
servers.CancelQuery( Request );
|
||||
Request = IntPtr.Zero;
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if ( changes && OnUpdate != null )
|
||||
OnUpdate();
|
||||
|
||||
return Request == IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public Action OnUpdate;
|
||||
public Action<Server> OnServerResponded;
|
||||
public Action OnFinished;
|
||||
|
||||
/// <summary>
|
||||
/// A list of servers that responded. If you're only interested in servers that responded since you
|
||||
/// last updated, then simply clear this list.
|
||||
/// </summary>
|
||||
public List<Server> Responded = new List<Server>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of servers that were in the master list but didn't respond.
|
||||
/// </summary>
|
||||
public List<Server> Unresponsive = new List<Server>();
|
||||
|
||||
/// <summary>
|
||||
/// True when we have finished
|
||||
/// </summary>
|
||||
public bool Finished = false;
|
||||
|
||||
internal Request( Client c )
|
||||
{
|
||||
client = c;
|
||||
|
||||
client.OnUpdate += Update;
|
||||
}
|
||||
|
||||
~Request()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
internal IEnumerable<string> ServerList { get; set; }
|
||||
internal Filter Filter { get; set; }
|
||||
|
||||
internal void StartCustomQuery()
|
||||
{
|
||||
if ( ServerList == null )
|
||||
return;
|
||||
|
||||
int blockSize = 16;
|
||||
int Pointer = 0;
|
||||
|
||||
while ( true )
|
||||
{
|
||||
var sublist = ServerList.Skip( Pointer ).Take( blockSize );
|
||||
|
||||
if ( sublist.Count() == 0 )
|
||||
break;
|
||||
|
||||
Pointer += sublist.Count();
|
||||
|
||||
var filter = new Filter();
|
||||
filter.Add( "or", sublist.Count().ToString() );
|
||||
|
||||
foreach ( var server in sublist )
|
||||
{
|
||||
filter.Add( "gameaddr", server );
|
||||
}
|
||||
|
||||
filter.Start();
|
||||
var id = client.native.servers.RequestInternetServerList( client.AppId, filter.NativeArray, (uint)filter.Count, IntPtr.Zero );
|
||||
filter.Free();
|
||||
|
||||
AddRequest( id );
|
||||
}
|
||||
|
||||
ServerList = null;
|
||||
}
|
||||
|
||||
internal void AddRequest( IntPtr id )
|
||||
{
|
||||
Requests.Add( new SubRequest() { Request = id } );
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if ( Requests.Count == 0 )
|
||||
return;
|
||||
|
||||
for( int i=0; i< Requests.Count(); i++ )
|
||||
{
|
||||
if ( Requests[i].Update( client.native.servers, OnServer, OnUpdate ) )
|
||||
{
|
||||
Requests.RemoveAt( i );
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if ( Requests.Count == 0 )
|
||||
{
|
||||
Finished = true;
|
||||
client.OnUpdate -= Update;
|
||||
|
||||
OnFinished?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServer( SteamNative.gameserveritem_t info )
|
||||
{
|
||||
if ( info.HadSuccessfulResponse )
|
||||
{
|
||||
if ( Filter != null && !Filter.Test( info ) )
|
||||
return;
|
||||
|
||||
var s = Server.FromSteam( client, info );
|
||||
Responded.Add( s );
|
||||
|
||||
OnServerResponded?.Invoke( s );
|
||||
}
|
||||
else
|
||||
{
|
||||
Unresponsive.Add( Server.FromSteam( client, info ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposing will end the query
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if ( client.IsValid )
|
||||
client.OnUpdate -= Update;
|
||||
|
||||
//
|
||||
// Cancel the query if it's still running
|
||||
//
|
||||
foreach( var subRequest in Requests )
|
||||
{
|
||||
if ( client.IsValid )
|
||||
client.native.servers.CancelQuery( subRequest.Request );
|
||||
}
|
||||
Requests.Clear();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class ServerList
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Ping { get; set; }
|
||||
public string GameDir { get; set; }
|
||||
public string Map { get; set; }
|
||||
public string Description { get; set; }
|
||||
public uint AppId { get; set; }
|
||||
public int Players { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public int BotPlayers { get; set; }
|
||||
public bool Passworded { get; set; }
|
||||
public bool Secure { get; set; }
|
||||
public uint LastTimePlayed { get; set; }
|
||||
public int Version { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
public ulong SteamId { get; set; }
|
||||
public IPAddress Address { get; set; }
|
||||
public int ConnectionPort { get; set; }
|
||||
public int QueryPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this server is in the favourites list
|
||||
/// </summary>
|
||||
public bool Favourite
|
||||
{
|
||||
get
|
||||
{
|
||||
return Client.ServerList.IsFavourite( this );
|
||||
}
|
||||
}
|
||||
|
||||
internal Client Client;
|
||||
|
||||
|
||||
internal static Server FromSteam( Client client, SteamNative.gameserveritem_t item )
|
||||
{
|
||||
return new Server()
|
||||
{
|
||||
Client = client,
|
||||
Address = Utility.Int32ToIp( item.NetAdr.IP ),
|
||||
ConnectionPort = item.NetAdr.ConnectionPort,
|
||||
QueryPort = item.NetAdr.QueryPort,
|
||||
Name = item.ServerName,
|
||||
Ping = item.Ping,
|
||||
GameDir = item.GameDir,
|
||||
Map = item.Map,
|
||||
Description = item.GameDescription,
|
||||
AppId = item.AppID,
|
||||
Players = item.Players,
|
||||
MaxPlayers = item.MaxPlayers,
|
||||
BotPlayers = item.BotPlayers,
|
||||
Passworded = item.Password,
|
||||
Secure = item.Secure,
|
||||
LastTimePlayed = item.TimeLastPlayed,
|
||||
Version = item.ServerVersion,
|
||||
Tags = item.GameTags == null ? null : item.GameTags.Split( ',' ),
|
||||
SteamId = item.SteamID
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback when rules are receieved.
|
||||
/// The bool is true if server responded properly.
|
||||
/// </summary>
|
||||
public Action<bool> OnReceivedRules;
|
||||
|
||||
/// <summary>
|
||||
/// List of server rules. Use HasRules to see if this is safe to access.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this server has rules
|
||||
/// </summary>
|
||||
public bool HasRules { get { return Rules != null && Rules.Count > 0; } }
|
||||
|
||||
internal SourceServerQuery RulesRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Populates Rules for this server
|
||||
/// </summary>
|
||||
public void FetchRules()
|
||||
{
|
||||
if ( RulesRequest != null )
|
||||
return;
|
||||
|
||||
Rules = null;
|
||||
RulesRequest = new SourceServerQuery( this, Address, QueryPort );
|
||||
}
|
||||
|
||||
internal void OnServerRulesReceiveFinished( Dictionary<string, string> rules, bool Success )
|
||||
{
|
||||
RulesRequest = null;
|
||||
|
||||
if ( Success )
|
||||
{
|
||||
Rules = rules;
|
||||
}
|
||||
|
||||
if ( OnReceivedRules != null )
|
||||
{
|
||||
OnReceivedRules( Success );
|
||||
}
|
||||
}
|
||||
|
||||
internal const uint k_unFavoriteFlagNone = 0x00;
|
||||
internal const uint k_unFavoriteFlagFavorite = 0x01; // this game favorite entry is for the favorites list
|
||||
internal const uint k_unFavoriteFlagHistory = 0x02; // this game favorite entry is for the history list
|
||||
|
||||
/// <summary>
|
||||
/// Add this server to our history list
|
||||
/// If we're already in the history list, weill set the last played time to now
|
||||
/// </summary>
|
||||
public void AddToHistory()
|
||||
{
|
||||
Client.native.matchmaking.AddFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Utility.Epoch.Current );
|
||||
Client.ServerList.UpdateFavouriteList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove this server from our history list
|
||||
/// </summary>
|
||||
public void RemoveFromHistory()
|
||||
{
|
||||
Client.native.matchmaking.RemoveFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory );
|
||||
Client.ServerList.UpdateFavouriteList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add this server to our favourite list
|
||||
/// </summary>
|
||||
public void AddToFavourites()
|
||||
{
|
||||
Client.native.matchmaking.AddFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Utility.Epoch.Current );
|
||||
Client.ServerList.UpdateFavouriteList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove this server from our favourite list
|
||||
/// </summary>
|
||||
public void RemoveFromFavourites()
|
||||
{
|
||||
Client.native.matchmaking.RemoveFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite );
|
||||
Client.ServerList.UpdateFavouriteList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class ServerList : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
internal ServerList( Client client )
|
||||
{
|
||||
this.client = client;
|
||||
|
||||
UpdateFavouriteList();
|
||||
}
|
||||
|
||||
HashSet<ulong> FavouriteHash = new HashSet<ulong>();
|
||||
HashSet<ulong> HistoryHash = new HashSet<ulong>();
|
||||
|
||||
internal void UpdateFavouriteList()
|
||||
{
|
||||
FavouriteHash.Clear();
|
||||
HistoryHash.Clear();
|
||||
|
||||
for ( int i=0; i< client.native.matchmaking.GetFavoriteGameCount(); i++ )
|
||||
{
|
||||
AppId_t appid = 0;
|
||||
uint ip;
|
||||
ushort conPort;
|
||||
ushort queryPort;
|
||||
uint lastplayed;
|
||||
uint flags;
|
||||
|
||||
client.native.matchmaking.GetFavoriteGame( i, ref appid, out ip, out conPort, out queryPort, out flags, out lastplayed );
|
||||
|
||||
ulong encoded = ip;
|
||||
encoded = encoded << 32;
|
||||
encoded = encoded | (uint)conPort;
|
||||
|
||||
if ( ( flags & Server.k_unFavoriteFlagFavorite ) == Server.k_unFavoriteFlagFavorite )
|
||||
FavouriteHash.Add( encoded );
|
||||
|
||||
if ( ( flags & Server.k_unFavoriteFlagFavorite ) == Server.k_unFavoriteFlagFavorite )
|
||||
HistoryHash.Add( encoded );
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
public class Filter : List<KeyValuePair<string, string>>
|
||||
{
|
||||
public void Add( string k, string v )
|
||||
{
|
||||
Add( new KeyValuePair<string, string>( k, v ) );
|
||||
}
|
||||
|
||||
internal IntPtr NativeArray;
|
||||
private IntPtr m_pArrayEntries;
|
||||
|
||||
private int AppId = 0;
|
||||
|
||||
internal void Start()
|
||||
{
|
||||
var filters = this.Select( x =>
|
||||
{
|
||||
if ( x.Key == "appid" ) AppId = int.Parse( x.Value );
|
||||
|
||||
return new SteamNative.MatchMakingKeyValuePair_t()
|
||||
{
|
||||
Key = x.Key,
|
||||
Value = x.Value
|
||||
};
|
||||
} ).ToArray();
|
||||
|
||||
int sizeOfMMKVP = Marshal.SizeOf(typeof(SteamNative.MatchMakingKeyValuePair_t));
|
||||
NativeArray = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( IntPtr ) ) * filters.Length );
|
||||
m_pArrayEntries = Marshal.AllocHGlobal( sizeOfMMKVP * filters.Length );
|
||||
|
||||
for ( int i = 0; i < filters.Length; ++i )
|
||||
{
|
||||
Marshal.StructureToPtr( filters[i], new IntPtr( m_pArrayEntries.ToInt64() + ( i * sizeOfMMKVP ) ), false );
|
||||
}
|
||||
|
||||
Marshal.WriteIntPtr( NativeArray, m_pArrayEntries );
|
||||
}
|
||||
|
||||
internal void Free()
|
||||
{
|
||||
if ( m_pArrayEntries != IntPtr.Zero )
|
||||
{
|
||||
Marshal.FreeHGlobal( m_pArrayEntries );
|
||||
}
|
||||
|
||||
if ( NativeArray != IntPtr.Zero )
|
||||
{
|
||||
Marshal.FreeHGlobal( NativeArray );
|
||||
}
|
||||
}
|
||||
|
||||
internal bool Test( gameserveritem_t info )
|
||||
{
|
||||
if ( AppId != 0 && AppId != info.AppID )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[StructLayout( LayoutKind.Sequential )]
|
||||
private struct MatchPair
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string key;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Request Internet( Filter filter = null )
|
||||
{
|
||||
if ( filter == null )
|
||||
{
|
||||
filter = new Filter();
|
||||
filter.Add( "appid", client.AppId.ToString() );
|
||||
}
|
||||
|
||||
filter.Start();
|
||||
|
||||
var request = new Request( client );
|
||||
request.Filter = filter;
|
||||
request.AddRequest( client.native.servers.RequestInternetServerList( client.AppId, filter.NativeArray, (uint) filter.Count, IntPtr.Zero ) );
|
||||
|
||||
filter.Free();
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query a list of addresses. No filters applied.
|
||||
/// </summary>
|
||||
public Request Custom( IEnumerable<string> serverList )
|
||||
{
|
||||
var request = new Request( client );
|
||||
request.ServerList = serverList;
|
||||
request.StartCustomQuery();
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a list of servers we've been on. History isn't applied automatically
|
||||
/// You need to call server.AddtoHistoryList() when you join a server etc.
|
||||
/// </summary>
|
||||
public Request History( Filter filter = null )
|
||||
{
|
||||
if ( filter == null )
|
||||
{
|
||||
filter = new Filter();
|
||||
filter.Add( "appid", client.AppId.ToString() );
|
||||
}
|
||||
|
||||
filter.Start();
|
||||
|
||||
var request = new Request( client );
|
||||
request.Filter = filter;
|
||||
request.AddRequest( client.native.servers.RequestHistoryServerList( client.AppId, filter.NativeArray, (uint)filter.Count, IntPtr.Zero ) );
|
||||
|
||||
filter.Free();
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a list of servers we've favourited
|
||||
/// </summary>
|
||||
public Request Favourites( Filter filter = null )
|
||||
{
|
||||
if ( filter == null )
|
||||
{
|
||||
filter = new Filter();
|
||||
filter.Add( "appid", client.AppId.ToString() );
|
||||
}
|
||||
|
||||
filter.Start();
|
||||
|
||||
var request = new Request( client );
|
||||
request.Filter = filter;
|
||||
request.AddRequest( client.native.servers.RequestFavoritesServerList( client.AppId, filter.NativeArray, (uint)filter.Count, IntPtr.Zero ) );
|
||||
|
||||
filter.Free();
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a list of servers that our friends are on
|
||||
/// </summary>
|
||||
public Request Friends( Filter filter = null )
|
||||
{
|
||||
if ( filter == null )
|
||||
{
|
||||
filter = new Filter();
|
||||
filter.Add( "appid", client.AppId.ToString() );
|
||||
}
|
||||
|
||||
filter.Start();
|
||||
|
||||
var request = new Request( client );
|
||||
request.Filter = filter;
|
||||
request.AddRequest( client.native.servers.RequestFriendsServerList( client.AppId, filter.NativeArray, (uint)filter.Count, IntPtr.Zero ) );
|
||||
|
||||
filter.Free();
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a list of servers that are running on our LAN
|
||||
/// </summary>
|
||||
public Request Local( Filter filter = null )
|
||||
{
|
||||
if ( filter == null )
|
||||
{
|
||||
filter = new Filter();
|
||||
filter.Add( "appid", client.AppId.ToString() );
|
||||
}
|
||||
|
||||
filter.Start();
|
||||
|
||||
var request = new Request( client );
|
||||
request.Filter = filter;
|
||||
request.AddRequest( client.native.servers.RequestLANServerList( client.AppId, IntPtr.Zero ) );
|
||||
|
||||
filter.Free();
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
internal bool IsFavourite( Server server )
|
||||
{
|
||||
ulong encoded = Utility.IpToInt32( server.Address );
|
||||
encoded = encoded << 32;
|
||||
encoded = encoded | (uint)server.ConnectionPort;
|
||||
|
||||
return FavouriteHash.Contains( encoded );
|
||||
}
|
||||
|
||||
internal bool IsHistory( Server server )
|
||||
{
|
||||
ulong encoded = Utility.IpToInt32( server.Address );
|
||||
encoded = encoded << 32;
|
||||
encoded = encoded | (uint)server.ConnectionPort;
|
||||
|
||||
return HistoryHash.Contains( encoded );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class Stats : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
internal Stats( Client c )
|
||||
{
|
||||
client = c;
|
||||
}
|
||||
|
||||
public bool StoreStats()
|
||||
{
|
||||
return client.native.userstats.StoreStats();
|
||||
}
|
||||
|
||||
public void UpdateStats()
|
||||
{
|
||||
client.native.userstats.RequestCurrentStats();
|
||||
}
|
||||
|
||||
public void UpdateGlobalStats( int days = 1 )
|
||||
{
|
||||
client.native.userstats.GetNumberOfCurrentPlayers();
|
||||
client.native.userstats.RequestGlobalAchievementPercentages();
|
||||
client.native.userstats.RequestGlobalStats( days );
|
||||
}
|
||||
|
||||
public int GetInt( string name )
|
||||
{
|
||||
int data = 0;
|
||||
client.native.userstats.GetStat( name, out data );
|
||||
return data;
|
||||
}
|
||||
|
||||
public long GetGlobalInt( string name )
|
||||
{
|
||||
long data = 0;
|
||||
client.native.userstats.GetGlobalStat( name, out data );
|
||||
return data;
|
||||
}
|
||||
|
||||
public float GetFloat( string name )
|
||||
{
|
||||
float data = 0;
|
||||
client.native.userstats.GetStat0( name, out data );
|
||||
return data;
|
||||
}
|
||||
|
||||
public double GetGlobalFloat( string name )
|
||||
{
|
||||
double data = 0;
|
||||
client.native.userstats.GetGlobalStat0( name, out data );
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a stat value. This will automatically call StoreStats() after a successful call
|
||||
/// unless you pass false as the last argument.
|
||||
/// </summary>
|
||||
public bool Set( string name, int value, bool store = true )
|
||||
{
|
||||
var r = client.native.userstats.SetStat( name, value );
|
||||
|
||||
if ( store )
|
||||
{
|
||||
return r && client.native.userstats.StoreStats();
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a stat value. This will automatically call StoreStats() after a successful call
|
||||
/// unless you pass false as the last argument.
|
||||
/// </summary>
|
||||
public bool Set( string name, float value, bool store = true )
|
||||
{
|
||||
var r = client.native.userstats.SetStat0( name, value );
|
||||
|
||||
if ( store )
|
||||
{
|
||||
return r && client.native.userstats.StoreStats();
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds this amount to the named stat. Internally this calls Get() and adds
|
||||
/// to that value. Steam doesn't provide a mechanism for atomically increasing
|
||||
/// stats like this, this functionality is added here as a convenience.
|
||||
/// </summary>
|
||||
public bool Add( string name, int amount = 1, bool store = true )
|
||||
{
|
||||
var val = GetInt( name );
|
||||
val += amount;
|
||||
return Set( name, val, store );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds this amount to the named stat. Internally this calls Get() and adds
|
||||
/// to that value. Steam doesn't provide a mechanism for atomically increasing
|
||||
/// stats like this, this functionality is added here as a convenience.
|
||||
/// </summary>
|
||||
public bool Add(string name, float amount = 1.0f, bool store = true)
|
||||
{
|
||||
var val = GetFloat(name);
|
||||
val += amount;
|
||||
return Set(name, val, store);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Practically wipes the slate clean for this user. If includeAchievements is true, will wipe
|
||||
/// any achievements too.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool ResetAll( bool includeAchievements )
|
||||
{
|
||||
return client.native.userstats.ResetAllStats( includeAchievements );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class SteamFriend
|
||||
{
|
||||
/// <summary>
|
||||
/// Steam Id
|
||||
/// </summary>
|
||||
public ulong Id { get; internal set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return true if blocked
|
||||
/// </summary>
|
||||
public bool IsBlocked => relationship == FriendRelationship.Blocked;
|
||||
|
||||
/// <summary>
|
||||
/// Return true if is a friend. Returns false if blocked, request etc.
|
||||
/// </summary>
|
||||
public bool IsFriend => relationship == FriendRelationship.Friend;
|
||||
|
||||
/// <summary>
|
||||
/// Their current display name
|
||||
/// </summary>
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this friend is online
|
||||
/// </summary>
|
||||
public bool IsOnline => personaState != PersonaState.Offline;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this friend is marked as away
|
||||
/// </summary>
|
||||
public bool IsAway => personaState == PersonaState.Away;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this friend is marked as busy
|
||||
/// </summary>
|
||||
public bool IsBusy => personaState == PersonaState.Busy;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this friend is marked as snoozing
|
||||
/// </summary>
|
||||
public bool IsSnoozing => personaState == PersonaState.Snooze;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this friend is online and playing this game
|
||||
/// </summary>
|
||||
public bool IsPlayingThisGame => CurrentAppId == Client.AppId;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this friend is online and playing this game
|
||||
/// </summary>
|
||||
public bool IsPlaying => CurrentAppId != 0;
|
||||
|
||||
/// <summary>
|
||||
/// The AppId this guy is playing
|
||||
/// </summary>
|
||||
public ulong CurrentAppId { get; internal set; }
|
||||
|
||||
public uint ServerIp { get; internal set; }
|
||||
public int ServerGamePort { get; internal set; }
|
||||
public int ServerQueryPort { get; internal set; }
|
||||
public ulong ServerLobbyId { get; internal set; }
|
||||
|
||||
internal Client Client { get; set; }
|
||||
private PersonaState personaState;
|
||||
private FriendRelationship relationship;
|
||||
|
||||
/// <summary>
|
||||
/// Returns null if the value doesn't exist
|
||||
/// </summary>
|
||||
public string GetRichPresence( string key )
|
||||
{
|
||||
var val = Client.native.friends.GetFriendRichPresence( Id, key );
|
||||
if ( string.IsNullOrEmpty( val ) ) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update this friend, request the latest data from Steam's servers
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
Name = Client.native.friends.GetFriendPersonaName( Id );
|
||||
|
||||
relationship = Client.native.friends.GetFriendRelationship( Id );
|
||||
personaState = Client.native.friends.GetFriendPersonaState( Id );
|
||||
|
||||
CurrentAppId = 0;
|
||||
ServerIp = 0;
|
||||
ServerGamePort = 0;
|
||||
ServerQueryPort = 0;
|
||||
ServerLobbyId = 0;
|
||||
|
||||
var gameInfo = new SteamNative.FriendGameInfo_t();
|
||||
if ( Client.native.friends.GetFriendGamePlayed( Id, ref gameInfo ) && gameInfo.GameID > 0 )
|
||||
{
|
||||
CurrentAppId = gameInfo.GameID;
|
||||
ServerIp = gameInfo.GameIP;
|
||||
ServerGamePort = gameInfo.GamePort;
|
||||
ServerQueryPort = gameInfo.QueryPort;
|
||||
ServerLobbyId = gameInfo.SteamIDLobby;
|
||||
}
|
||||
|
||||
Client.native.friends.RequestFriendRichPresence( Id );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will return null if you don't have the target user's avatar in your cache.
|
||||
/// Which usually happens for people not on your friends list.
|
||||
/// </summary>
|
||||
public Image GetAvatar( Friends.AvatarSize size )
|
||||
{
|
||||
return Client.Friends.GetCachedAvatar( size, Id );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invite this friend to the game that we are playing
|
||||
/// </summary>
|
||||
public bool InviteToGame(string Text)
|
||||
{
|
||||
return Client.native.friends.InviteUserToGame(Id, Text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a Steam friend. Returns true if success
|
||||
/// </summary>
|
||||
public bool SendMessage( string message )
|
||||
{
|
||||
return Client.native.friends.ReplyToFriendMessage( Id, message );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using SteamNative;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class User : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
internal Dictionary<string, string> richPresence = new Dictionary<string, string>();
|
||||
|
||||
internal User( Client c )
|
||||
{
|
||||
client = c;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find a rich presence value by key for current user. Will be null if not found.
|
||||
/// </summary>
|
||||
public string GetRichPresence( string key )
|
||||
{
|
||||
if ( richPresence.TryGetValue( key, out var val ) )
|
||||
return val;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a rich presence value by key for current user.
|
||||
/// </summary>
|
||||
public bool SetRichPresence( string key, string value )
|
||||
{
|
||||
richPresence[key] = value;
|
||||
return client.native.friends.SetRichPresence( key, value );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all of the current user's rich presence data.
|
||||
/// </summary>
|
||||
public void ClearRichPresence()
|
||||
{
|
||||
richPresence.Clear();
|
||||
client.native.friends.ClearRichPresence();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public class Voice
|
||||
{
|
||||
const int ReadBufferSize = 1024 * 128;
|
||||
|
||||
internal Client client;
|
||||
|
||||
internal byte[] ReadCompressedBuffer = new byte[ReadBufferSize];
|
||||
internal byte[] ReadUncompressedBuffer = new byte[ReadBufferSize];
|
||||
|
||||
internal byte[] UncompressBuffer = new byte[1024 * 256];
|
||||
|
||||
public Action<byte[], int> OnCompressedData;
|
||||
public Action<byte[], int> OnUncompressedData;
|
||||
|
||||
private System.Diagnostics.Stopwatch UpdateTimer = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the optimal sample rate for voice - according to Steam
|
||||
/// </summary>
|
||||
public uint OptimalSampleRate
|
||||
{
|
||||
get { return client.native.user.GetVoiceOptimalSampleRate(); }
|
||||
}
|
||||
|
||||
private bool _wantsrecording = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// If set to true we are listening to the mic.
|
||||
/// You should usually toggle this with the press of a key for push to talk.
|
||||
/// </summary>
|
||||
public bool WantsRecording
|
||||
{
|
||||
get { return _wantsrecording; }
|
||||
set
|
||||
{
|
||||
_wantsrecording = value;
|
||||
|
||||
if ( value )
|
||||
{
|
||||
client.native.user.StartVoiceRecording();
|
||||
}
|
||||
else
|
||||
{
|
||||
client.native.user.StopVoiceRecording();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The last time voice was detected, recorded
|
||||
/// </summary>
|
||||
public DateTime LastVoiceRecordTime { get; private set; }
|
||||
|
||||
public TimeSpan TimeSinceLastVoiceRecord { get { return DateTime.Now.Subtract( LastVoiceRecordTime ); } }
|
||||
|
||||
public bool IsRecording = false;
|
||||
|
||||
/// <summary>
|
||||
/// If set we will capture the audio at this rate. If unset (set to 0) will capture at OptimalSampleRate
|
||||
/// </summary>
|
||||
public uint DesiredSampleRate = 0;
|
||||
|
||||
internal Voice( Client client )
|
||||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This gets called inside Update - so there's no need to call this manually if you're calling update
|
||||
/// </summary>
|
||||
public unsafe void Update()
|
||||
{
|
||||
if ( OnCompressedData == null && OnUncompressedData == null )
|
||||
return;
|
||||
|
||||
if ( UpdateTimer.Elapsed.TotalSeconds < 1.0f / 10.0f )
|
||||
return;
|
||||
|
||||
UpdateTimer.Reset();
|
||||
UpdateTimer.Start();
|
||||
|
||||
uint bufferRegularLastWrite = 0;
|
||||
uint bufferCompressedLastWrite = 0;
|
||||
|
||||
var result = client.native.user.GetAvailableVoice( out bufferCompressedLastWrite, out bufferRegularLastWrite, DesiredSampleRate == 0 ? OptimalSampleRate : DesiredSampleRate );
|
||||
|
||||
if ( result == SteamNative.VoiceResult.NotRecording || result == SteamNative.VoiceResult.NotInitialized )
|
||||
{
|
||||
IsRecording = false;
|
||||
return;
|
||||
}
|
||||
|
||||
fixed (byte* compressedPtr = ReadCompressedBuffer)
|
||||
fixed (byte* uncompressedPtr = ReadUncompressedBuffer)
|
||||
{
|
||||
result = client.native.user.GetVoice( OnCompressedData != null, (IntPtr) compressedPtr, ReadBufferSize, out bufferCompressedLastWrite,
|
||||
OnUncompressedData != null, (IntPtr) uncompressedPtr, ReadBufferSize, out bufferRegularLastWrite,
|
||||
DesiredSampleRate == 0 ? OptimalSampleRate : DesiredSampleRate );
|
||||
}
|
||||
|
||||
IsRecording = true;
|
||||
|
||||
if ( result == SteamNative.VoiceResult.OK )
|
||||
{
|
||||
if ( OnCompressedData != null && bufferCompressedLastWrite > 0 )
|
||||
{
|
||||
OnCompressedData( ReadCompressedBuffer, (int)bufferCompressedLastWrite );
|
||||
}
|
||||
|
||||
if ( OnUncompressedData != null && bufferRegularLastWrite > 0 )
|
||||
{
|
||||
OnUncompressedData( ReadUncompressedBuffer, (int)bufferRegularLastWrite );
|
||||
}
|
||||
|
||||
LastVoiceRecordTime = DateTime.Now;
|
||||
}
|
||||
|
||||
if ( result == SteamNative.VoiceResult.NotRecording ||
|
||||
result == SteamNative.VoiceResult.NotInitialized )
|
||||
IsRecording = false;
|
||||
|
||||
}
|
||||
|
||||
public bool Decompress( byte[] input, MemoryStream output, uint samepleRate = 0 )
|
||||
{
|
||||
return Decompress( input, 0, input.Length, output, samepleRate );
|
||||
}
|
||||
|
||||
public bool Decompress( byte[] input, int inputsize, MemoryStream output, uint samepleRate = 0 )
|
||||
{
|
||||
return Decompress( input, 0, inputsize, output, samepleRate );
|
||||
}
|
||||
|
||||
public unsafe bool Decompress( byte[] input, int inputoffset, int inputsize, MemoryStream output, uint samepleRate = 0 )
|
||||
{
|
||||
if ( inputoffset < 0 || inputoffset >= input.Length )
|
||||
throw new ArgumentOutOfRangeException( "inputoffset" );
|
||||
|
||||
if ( inputsize <= 0 || inputoffset + inputsize > input.Length )
|
||||
throw new ArgumentOutOfRangeException( "inputsize" );
|
||||
|
||||
fixed ( byte* p = input )
|
||||
{
|
||||
return Decompress( (IntPtr)p, inputoffset, inputsize, output, samepleRate );
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool Decompress( IntPtr input, int inputoffset, int inputsize, MemoryStream output, uint samepleRate = 0 )
|
||||
{
|
||||
if ( samepleRate == 0 )
|
||||
samepleRate = OptimalSampleRate;
|
||||
|
||||
uint bytesOut = 0;
|
||||
|
||||
SteamNative.VoiceResult result = SteamNative.VoiceResult.NoData;
|
||||
|
||||
fixed ( byte* outBuf = UncompressBuffer )
|
||||
{
|
||||
result = client.native.user.DecompressVoice( (IntPtr)(((byte*)input) + inputoffset), (uint)inputsize, (IntPtr)outBuf, (uint)UncompressBuffer.Length, out bytesOut, samepleRate );
|
||||
}
|
||||
|
||||
if ( result == SteamNative.VoiceResult.OK )
|
||||
{
|
||||
output.Write( (byte[])UncompressBuffer, 0, (int)bytesOut );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user