using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Steamworks.Data; namespace Steamworks { /// /// Class for utilizing the Steam Friends API. /// public class SteamFriends : SteamClientClass { internal static ISteamFriends? Internal => Interface as ISteamFriends; internal override bool InitializeInterface( bool server ) { SetInterface( server, new ISteamFriends( server ) ); if ( Interface is null || Interface.Self == IntPtr.Zero ) return false; richPresence = new Dictionary(); InstallEvents(); return true; } static Dictionary? richPresence; internal void InstallEvents() { Dispatch.Install( x => OnPersonaStateChange?.Invoke( new Friend( x.SteamID ) ) ); Dispatch.Install( x => OnGameRichPresenceJoinRequested?.Invoke( new Friend( x.SteamIDFriend), x.ConnectUTF8() ) ); Dispatch.Install( OnFriendChatMessage ); Dispatch.Install( OnGameConnectedClanChatMessage ); Dispatch.Install( x => OnGameOverlayActivated?.Invoke( x.Active != 0 ) ); Dispatch.Install( x => OnGameServerChangeRequested?.Invoke( x.ServerUTF8(), x.PasswordUTF8() ) ); Dispatch.Install( x => OnGameLobbyJoinRequested?.Invoke( new Lobby( x.SteamIDLobby ), x.SteamIDFriend ) ); Dispatch.Install( x => OnFriendRichPresenceUpdate?.Invoke( new Friend( x.SteamIDFriend ) ) ); Dispatch.Install( x => OnOverlayBrowserProtocol?.Invoke( x.RgchURIUTF8() ) ); } /// /// Invoked when a chat message has been received from a friend. You'll need to enable /// to recieve this. (friend, msgtype, message) /// public static event Action? OnChatMessage; /// /// Invoked when a chat message has been received in a Steam group chat that we are in. Associated Functions: JoinClanChatRoom. (friend, msgtype, message) /// public static event Action? OnClanChatMessage; /// /// Invoked when a friends' status changes. /// public static event Action? OnPersonaStateChange; /// /// Invoked when the user tries to join a game from their friends list. /// Rich presence will have been set with the "connect" key which is set here. /// public static event Action? OnGameRichPresenceJoinRequested; /// /// Invoked when game overlay activates or deactivates. /// The game can use this to be pause or resume single player games. /// public static event Action? OnGameOverlayActivated; /// /// Invoked when the user tries to join a different game server from their friends list. /// Game client should attempt to connect to specified server when this is received. /// public static event Action? OnGameServerChangeRequested; /// /// Invoked when the user tries to join a lobby from their friends list. /// Game client should attempt to connect to specified lobby when this is received. /// public static event Action? OnGameLobbyJoinRequested; /// /// Invoked when a friend's rich presence data is updated. /// public static event Action? OnFriendRichPresenceUpdate; /// /// Invoked when an overlay browser instance is navigated to a /// protocol/scheme registered by . /// public static event Action? OnOverlayBrowserProtocol; static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data ) { if ( OnChatMessage == null ) return; var friend = new Friend( data.SteamIDUser ); using var buffer = Helpers.TakeMemory(); var type = ChatEntryType.ChatMsg; var len = Internal?.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ) ?? 0; if ( len == 0 && type == ChatEntryType.Invalid ) return; var typeName = type.ToString(); var message = Helpers.MemoryToString( buffer ); OnChatMessage( friend, typeName, message ); } static unsafe void OnGameConnectedClanChatMessage( GameConnectedClanChatMsg_t data ) { if ( OnClanChatMessage == null ) return; if ( Internal is null ) return; var friend = new Friend( data.SteamIDUser ); using var buffer = Helpers.TakeMemory(); var type = ChatEntryType.ChatMsg; SteamId chatter = data.SteamIDUser; var len = Internal.GetClanChatMessage( data.SteamIDClanChat, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type, ref chatter ); if ( len == 0 && type == ChatEntryType.Invalid ) return; var typeName = type.ToString(); var message = Helpers.MemoryToString( buffer ); OnClanChatMessage( friend, typeName, message ); } private static IEnumerable GetFriendsWithFlag(FriendFlags flag) { if (Internal is null) { yield break; } int friendCount = Internal.GetFriendCount((int)flag); for ( int i=0; i /// Gets an of friends that the current user has. /// /// An of friends. public static IEnumerable GetFriends() { return GetFriendsWithFlag(FriendFlags.Immediate); } /// /// Gets an of blocked users that the current user has. /// /// An of blocked users. public static IEnumerable GetBlocked() { return GetFriendsWithFlag(FriendFlags.Blocked); } /// /// Gets an of friend requests that the current user has. /// /// An of friend requests. public static IEnumerable GetFriendsRequested() { return GetFriendsWithFlag( FriendFlags.FriendshipRequested ); } public static IEnumerable GetFriendsClanMembers() { return GetFriendsWithFlag( FriendFlags.ClanMember ); } public static IEnumerable GetFriendsOnGameServer() { return GetFriendsWithFlag( FriendFlags.OnGameServer ); } public static IEnumerable GetFriendsRequestingFriendship() { return GetFriendsWithFlag( FriendFlags.RequestingFriendship ); } public static IEnumerable GetPlayedWith() { if (Internal is null) { yield break; } int friendCount = Internal.GetCoplayFriendCount(); for ( int i = 0; i < friendCount; i++ ) { if (Internal is null) { yield break; } yield return new Friend( Internal.GetCoplayFriend( i ) ); } } public static IEnumerable GetFromSource( SteamId steamid ) { if (Internal is null) { yield break; } int friendCount = Internal.GetFriendCountFromSource( steamid ); for ( int i = 0; i < friendCount; i++ ) { if (Internal is null) { yield break; } yield return new Friend( Internal.GetFriendFromSourceByIndex( steamid, i ) ); } } public static IEnumerable GetClans() { if (Internal is null) { yield break; } int friendCount = Internal.GetClanCount(); for ( int i = 0; i < friendCount; i++ ) { if (Internal is null) { yield break; } yield return new Clan( Internal.GetClanByIndex( i ) ); } } /// /// Opens a specific overlay window. Valid options are: /// "friends", /// "community", /// "players", /// "settings", /// "officialgamegroup", /// "stats", /// "achievements". /// public static void OpenOverlay( string type ) => Internal?.ActivateGameOverlay( type ); /// /// "steamid" - Opens the overlay web browser to the specified user or groups profile. /// "chat" - Opens a chat window to the specified user, or joins the group chat. /// "jointrade" - Opens a window to a Steam Trading session that was started with the ISteamEconomy/StartTrade Web API. /// "stats" - Opens the overlay web browser to the specified user's stats. /// "achievements" - Opens the overlay web browser to the specified user's achievements. /// "friendadd" - Opens the overlay in minimal mode prompting the user to add the target user as a friend. /// "friendremove" - Opens the overlay in minimal mode prompting the user to remove the target friend. /// "friendrequestaccept" - Opens the overlay in minimal mode prompting the user to accept an incoming friend invite. /// "friendrequestignore" - Opens the overlay in minimal mode prompting the user to ignore an incoming friend invite. /// public static void OpenUserOverlay( SteamId id, string type ) => Internal?.ActivateGameOverlayToUser( type, id ); /// /// Activates the Steam Overlay to the Steam store page for the provided app. /// public static void OpenStoreOverlay( AppId id, OverlayToStoreFlag overlayToStoreFlag = OverlayToStoreFlag.None ) => Internal?.ActivateGameOverlayToStore( id.Value, overlayToStoreFlag ); /// /// Activates Steam Overlay web browser directly to the specified URL. /// public static void OpenWebOverlay( string url, bool modal = false ) => Internal?.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); /// /// Activates the Steam Overlay to open the invite dialog. Invitations sent from this dialog will be for the provided lobby. /// public static void OpenGameInviteOverlay( SteamId lobby ) => Internal?.ActivateGameOverlayInviteDialog( lobby ); /// /// Mark a target user as 'played with'. /// NOTE: The current user must be in game with the other player for the association to work. /// public static void SetPlayedWith( SteamId steamid ) => Internal?.SetPlayedWith( steamid ); /// /// Requests the persona name and optionally the avatar of a specified user. /// NOTE: It's a lot slower to download avatars and churns the local cache, so if you don't need avatars, don't request them. /// returns true if we're fetching the data, false if we already have it /// public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal != null && Internal.RequestUserInformation( steamid, nameonly ); internal static async Task CacheUserInformationAsync( SteamId steamid, bool nameonly ) { // Got it straight away, skip any waiting. if ( !RequestUserInformation( steamid, nameonly ) ) return; await Task.Delay( 100 ); while ( RequestUserInformation( steamid, nameonly ) ) { await Task.Delay( 50 ); } // // And extra wait here seems to solve avatars loading as [?] // await Task.Delay( 500 ); } /// /// Returns a small avatar of the user with the given . /// /// The of the user to get. /// A with a value if the image was successfully retrieved. public static async Task GetSmallAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) ); } /// /// Returns a medium avatar of the user with the given . /// /// The of the user to get. /// A with a value if the image was successfully retrieved. public static async Task GetMediumAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) ); } /// /// Returns a large avatar of the user with the given . /// /// The of the user to get. /// A with a value if the image was successfully retrieved. public static async Task GetLargeAvatarAsync( SteamId steamid ) { if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); var imageid = Internal.GetLargeFriendAvatar( steamid ); // Wait for the image to download while ( imageid == -1 ) { await Task.Delay( 50 ); imageid = Internal.GetLargeFriendAvatar( steamid ); } return SteamUtils.GetImage( imageid ); } /// /// Find a rich presence value by key for current user. Will be null if not found. /// public static string? GetRichPresence( string key ) { if (richPresence is null) { return null; } if ( richPresence.TryGetValue( key, out var val ) ) return val; return null; } /// /// Sets a rich presence value by key for current user. /// public static bool SetRichPresence( string key, string value ) { if (richPresence is null || Internal is null) { return false; } bool success = Internal.SetRichPresence( key, value ); if ( success ) richPresence[key] = value; return success; } /// /// Clears all of the current user's rich presence data. /// public static void ClearRichPresence() { richPresence?.Clear(); Internal?.ClearRichPresence(); } static bool _listenForFriendsMessages; /// /// 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. /// public static bool ListenForFriendsMessages { get => _listenForFriendsMessages; set { _listenForFriendsMessages = value; Internal?.SetListenForFriendsMessages( value ); } } /// /// Gets whether or not the current user is following the user with the given . /// /// The to check. /// Boolean. public static async Task IsFollowing(SteamId steamID) { if (Internal is null) { return false; } var r = await Internal.IsFollowing(steamID); return r?.IsFollowing ?? false; } public static async Task GetFollowerCount(SteamId steamID) { if (Internal is null) { return 0; } var r = await Internal.GetFollowerCount(steamID); return r?.Count ?? 0; } public static async Task GetFollowingList() { int resultCount = 0; var steamIds = new List(); FriendsEnumerateFollowingList_t? result; do { if (Internal is null) { break; } if ( (result = await Internal.EnumerateFollowingList((uint)resultCount)) != null) { resultCount += result.Value.ResultsReturned; Array.ForEach(result.Value.GSteamID, id => { if (id > 0) steamIds.Add(id); }); } } while (result != null && resultCount < result.Value.TotalResultCount); return steamIds.ToArray(); } /// /// Call this before calling ActivateGameOverlayToWebPage() to have the Steam Overlay Browser block navigations /// to your specified protocol (scheme) uris and instead dispatch a OverlayBrowserProtocolNavigation callback to your game. /// public static bool RegisterProtocolInOverlayBrowser( string protocol ) { return Internal != null && Internal.RegisterProtocolInOverlayBrowser( protocol ); } public static async Task JoinClanChatRoom( SteamId chatId ) { if ( Internal is null ) return false; var result = await Internal.JoinClanChatRoom( chatId ); if ( !result.HasValue ) return false; return result.Value.ChatRoomEnterResponse == RoomEnter.Success ; } public static bool SendClanChatRoomMessage( SteamId chatId, string message ) { return Internal != null && Internal.SendClanChatMessage( chatId, message ); } } }